/* ========================================================================== */
/*! \file
 * \brief Configuration handling
 *
 * Copyright (c) 2012-2015 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, functions 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 <stdio.h>
#include <string.h>

#include "conf.h"
#include "config.h"
#include "fileutils.h"
#include "main.h"


/* ========================================================================== */
/*! \addtogroup CONF
 *
 * Location of configfile: \c ~/.$CFG_NAME/configfile
 *
 * Any line starting with \c # is treated as a comment (not parsed and ignored).
 *
 * To add a new entry:
 * - Add new entry type in file \ref conf.h to enum \ref conf_entry
 * - Increment \ref CONF_NUM in file \ref conf.h
 * - Modify static function \c conf_init_configuration() to init the new entry
 *   to a default
 */
/* @{ */


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

/*! \brief Permissions for config file */
#define CONF_PERM  (posix_mode_t) (POSIX_S_IRUSR | POSIX_S_IWUSR)


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

/*! \brief Global configuration
 *
 * There must be \ref CONF_NUM entries of type \ref conf in ascending order
 * starting from zero without holes, so that \ref conf_entry IDs can be used
 * as index.
 */
struct conf  config[CONF_NUM];

/*! \brief Flag indicating that password should not be stored in configfile
 *
 * If this flag is nonzero a call to \ref conf_store() will delete the password
 * from the configuration before writing it to disk.
 */
int  conf_ephemeral_passwd = 0;

static int  conf_initialized = 0;
static const char  confname[] = "configfile";
static const char  tmpname[] = "configfile.new";


/* ========================================================================== */
/* Add integer configuration entry */

static void  conf_add_entry_int(struct conf*  conf, enum conf_entry  entry,
                                const char*  label, int  val)
{
   conf[entry].label = label;
   conf[entry].type = CONF_TYPE_INT;
   conf[entry].val.i = val;
   conf[entry].found = 0;
}


/* ========================================================================== */
/* Add string configuration entry */

static void  conf_add_entry_string(struct conf*  conf, enum conf_entry  entry,
                                   const char*  label, const char*  val)
{
   conf[entry].label = label;
   conf[entry].type = CONF_TYPE_STRING;
   conf[entry].val.s = val;
   conf[entry].found = 0;
}


/* ========================================================================== */
/* Init configuration with default values (to use if there is no config file)
 *
 * \attention
 * The content of entries with string type must be stored in dynamic memory!
 * For empty strings, a memory block with NUL termination inside must be used.
 */

static void  conf_init_configuration(struct conf*  cfg)
{
   const char  server_default[] = "localhost";
   const char  service_default[] = "nntp";
   const char  test_ere_default[] = ".*\\.test$|^test$";
   const char  test_kwords_default[] = "ignore,no-reply";
   const char  crl_upd_ts_default[] = "1970-01-01T00:00:00Z";
   /* This is a format string, '%s' will be replaced with cited authors name */
   const char  intro_default[] = "%s wrote:";
   char*  string;
   char*  empty;
   int  error = 0;

   /* Main window size, position and tiling */
   conf_add_entry_int(cfg, CONF_POS_X, "pos_x:", 10);
   conf_add_entry_int(cfg, CONF_POS_Y, "pos_y:", 40);
   conf_add_entry_int(cfg, CONF_SIZE_X, "size_x:", 750);
   conf_add_entry_int(cfg, CONF_SIZE_Y, "size_y:", 350);
   conf_add_entry_int(cfg, CONF_TILE_X, "tile_x:", 200);
   conf_add_entry_int(cfg, CONF_TILE_Y, "tile_y:", 120);

   /* NNTP server */
   string = (char*) posix_malloc(strlen(server_default) + (size_t) 1);
   if(NULL == string)  { error = 1; }
   else
   {
      strcpy(string, server_default);
      conf_add_entry_string(cfg, CONF_SERVER, "server:", string);
   }

   /* NNTP service (port) */
   string = (char*) posix_malloc(strlen(service_default) + (size_t) 1);
   if(NULL == string)  { error = 1; }
   else
   {
      strcpy(string, service_default);
      conf_add_entry_string(cfg, CONF_SERVICE, "service:", string);
   }

   /* NNTP connection encryption */
   conf_add_entry_int(cfg, CONF_ENC, "enc:", 0);

   /* Authentication against NNTP server */
   conf_add_entry_int(cfg, CONF_AUTH, "auth:", 0);
   conf_add_entry_int(cfg, CONF_IMMEDAUTH, "immedauth:", 1);
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_USER, "user:", empty);
   }
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_PASS, "pass:", empty);
   }

   /* Clamp article count limit */
   conf_add_entry_int(cfg, CONF_CAC, "cac:", 250);

   /* "From" name and E-Mail address  */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_FROM, "from:", empty);
   }

   /* "Reply-To" name and E-Mail address  */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_REPLYTO, "replyto:", empty);
   }

   /* Domain name (for "Message-ID" creation) */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_DOMAIN, "domain:", empty);
   }

   /* Pathname of shared newsrc file */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_NEWSRC, "newsrc:", empty);
   }

   /* Pathname of shared scorerc file */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_SCORERC, "scorerc:", empty);
   }

   /* Introduction line format */
   string = (char*) posix_malloc(strlen(intro_default) + (size_t) 1);
   if(NULL == string)  { error = 1; }
   else
   {
      strcpy(string, intro_default);
      conf_add_entry_string(cfg, CONF_INTRO, "intro:", string);
   }

   /* Organization name */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_ORGANIZATION, "organization:", empty);
   }

   /* Threaded view */
   conf_add_entry_int(cfg, CONF_TVIEW, "tview:", 1);

   /* Show only unread articles (or threads containing unread articles) */
   conf_add_entry_int(cfg, CONF_ONLYUR, "onlyur:", 0);

   /* Secret for Cancel-Key */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_CANCELKEY, "cancelkey:", empty);
   }

   /* Pathname of external article postprocessor */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_PPROC, "post_proc:", empty);
   }

   /* Pathname of external article editor */
   empty = (char*) posix_malloc((size_t) 1);
   if(NULL == empty)  { error = 1; }
   else
   {
      empty[0] = 0;
      conf_add_entry_string(cfg, CONF_EDITOR, "editor:", empty);
   }

   /* Quoting style */
   conf_add_entry_int(cfg, CONF_QUOTESTYLE, "quote_style:", 1);

   /* Unification of quoting */
   conf_add_entry_int(cfg, CONF_QUOTEUNIFY, "quote_unify:", 1);

   /* POSIX ERE (Extended Regular Expression) to match test groups */
   string = (char*) posix_malloc(strlen(test_ere_default) + (size_t) 1);
   if(NULL == string)  { error = 1; }
   else
   {
      strcpy(string, test_ere_default);
      conf_add_entry_string(cfg, CONF_TESTGRP_ERE, "testgrp_ere:", string);
   }

   /* Keywords to set for test groups (on ERE match) */
   string = (char*) posix_malloc(strlen(test_kwords_default) + (size_t) 1);
   if(NULL == string)  { error = 1; }
   else
   {
      strcpy(string, test_kwords_default);
      conf_add_entry_string(cfg, CONF_TESTGRP_KWORDS, "testgrp_kwords:",
                            string);
   }

   /* TLS certificate CRL update interval (in hours) */
   conf_add_entry_int(cfg, CONF_CRL_UPD_IV, "crl_upd_iv:", 20);

   /* TLS certificate CRL update timestamp */
   string = (char*) posix_malloc(strlen(crl_upd_ts_default) + (size_t) 1);
   if(NULL == string)  { error = 1; }
   else
   {
      strcpy(string, crl_upd_ts_default);
      conf_add_entry_string(cfg, CONF_CRL_UPD_TS, "crl_upd_ts:", string);
   }

   /* TLS certificate CRL update choice (Allow user to skip update) */
   conf_add_entry_int(cfg, CONF_CRL_UPD_C, "crl_upd_c:", 0);

   /* Sort unthreaded view in GUI using article numbers */
   conf_add_entry_int(cfg, CONF_UTVIEW_AN, "utview_an:", 0);

   /* Sort articles in GUI with inverted order */
   conf_add_entry_int(cfg, CONF_INV_ORDER, "inv_order:", 0);

   /* NNTP driver don't use overview (OVER command) if nonzero */
   conf_add_entry_int(cfg, CONF_NO_OVER, "nntp_no_over:", 0);

   /* Use distribution suggestions from article source (if available) */
   conf_add_entry_int(cfg, CONF_DIST_SUGG, "dist_suggestions:", 0);

   /* Negotiate compression (if available) */
   conf_add_entry_int(cfg, CONF_COMPRESSION, "nntp_compression:", 0);

   /* On success, store flag that configuration was initialized */
   if(error)  { print_error("CFG: Initializing configuration failed"); }
   else  { conf_initialized = 1; }
}


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

static int  conf_get_confpathname(char**  confpathname, const char*  conffile)
{
   int  res = -1;
   const char*  homedir = NULL;
   const char  confdir[] = "/." CFG_NAME "/";
   int  rv;

   /* Get home directory from environment */
   rv = ts_getenv("HOME", &homedir);
   /* Prepare configuration directory */
   if(!rv)
   {
      /* Check home directory path */
      rv = fu_check_homedir(homedir);
      /* Create config file pathname */
      if(!rv)
      {
         /* The additional byte is for the terminating NUL */
         *confpathname = (char*) posix_malloc(strlen(homedir) + strlen(confdir)
                                              + strlen(conffile) + (size_t) 1);
         if(NULL == *confpathname)
         {
            print_error("CFG: Cannot allocate memory for config file pathname");
         }
         else
         {
            /* Create config directory if it doesn't exist */
            strcpy(*confpathname, homedir);
            strcat(*confpathname, confdir);
            rv = posix_mkdir(*confpathname, (posix_mode_t) POSIX_S_IRWXU);
            if(!rv || (-1 == rv && POSIX_EEXIST == posix_errno))
            {
               /* Configuration directory is now available */
               strcat(*confpathname, conffile);
               res = 0;
            }
            else  { print_error("CFG: Cannot create configuration directory"); }
         }
      }
      posix_free((void*) homedir);
   }
   else  { print_error("CFG: Environment variable 'HOME' is not defined"); }

   return(res);
}


/* ========================================================================== */
/* Extract integer value from configuration data */

static int  conf_extract_entry(char*  line, struct conf*  target)
{
   int  res = -1;
   size_t  labelsize = strlen(target->label);
   const char*  string;
   char*  sbuf;
   int  val;
   size_t  i;

   /* Check line length */
   if(strlen(line) > labelsize)
   {
      /* Search for label */
      if(!strncmp(target->label, line, labelsize))
      {
         /* Found */
         res = 0;
         if(CONF_TYPE_INT == target->type)
         {
            /* Extract integer value */
            string = &line[labelsize];
            if(1 == sscanf(string, "%d", &val))  target->val.i = val;
         }
         else
         {
            /* Extract string */
            i = labelsize;
            while(line[i])
            {
               if(' ' != line[i])  { break; }
               else  ++i;
            }
            string = &line[i];
            while(line[i])
            {
               if('\n' == line[i])
               {
                  line[i] = 0;
                  break;
               }
               else  ++i;
            }
            /*
             * Allocate memory for string entry
             * This memory is not released until the program exit.
             */
            sbuf = (char*) posix_malloc(strlen(string) + (size_t) 1);
            if(NULL == sbuf)
            {
               print_error("CFG: Cannot allocate memory for config file entry");
               target->val.s = NULL;
               res = -1;
            }
            else
            {
               strcpy(sbuf, string);
               posix_free((void*) target->val.s);
               target->val.s = (const char*) sbuf;
            }
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Update configuration data */

static int  conf_update_entry(char**  line, size_t  len, struct conf*  target)
{
   int  res = -1;
   size_t  labelsize = strlen(target->label);
   char*  string;
   int  reqd;

   /* Check line length */
   if(strlen(*line) >= labelsize)
   {
      /* Search for label */
      if(!strncmp(target->label, *line, labelsize))
      {
         /* Found => Insert new value */
         res = 0;
         target->found = 1;
         do
         {
            reqd = -1;
            if(CONF_TYPE_INT == target->type)
            {
               /* Update integer entry */
               reqd = posix_snprintf(*line, len, "%s %d\n",
                                     target->label, target->val.i);
            }
            if(CONF_TYPE_STRING == target->type)
            {
               /* Update string entry */
               reqd = posix_snprintf(*line, len, "%s %s\n",
                                     target->label, target->val.s);
            }
            if(0 > reqd)  { break; }
            else if(len <= (size_t) reqd)
            {
               /* Allocate more memory */
               if(POSIX_SIZE_MAX <= (size_t) reqd)  { break; }
               string = (char*) posix_realloc(*line, (size_t) (reqd + 1));
               if(NULL == string)  { break; }
               else  { *line = string; }
            }
         }
         while(len <= (size_t) reqd);
      }
   }

   return(res);
}


/* ========================================================================== */
/* Import configuration */

static int  conf_import_configuration(struct conf*  cfg, FILE*  fs)
{
   int  res = -1;
   char*  line = NULL;
   size_t  len = 0;
   posix_ssize_t  readlen;
   unsigned int  i;

   while(1)
   {
      /* Read line */
      readlen = posix_getline(&line, &len, fs);
      if(-1 == readlen)
      {
         if(POSIX_ENOMEM == posix_errno)
         {
            print_error("CFG: Cannot assign memory for config file parser");
         }
         else
         {
            /* Check for error */
            if(ferror(fs))
            {
               print_error("CFG: Parse error in config file");
               break;
            }
            /* Check for EOF */
            if(feof(fs))
            {
               res = 0;
               break;
            }
         }
      }
      if(0 >= readlen)  { break; }
      else
      {
         /* Extract data */
         if(!strncmp("#", line, 1))  { continue; }
         for(i = 0; i < CONF_NUM; ++i)
         {
            if(!conf_extract_entry(line, &cfg[i]))  { break; }
         }
      }
   }

   /* Release memory for line buffer */
   posix_free((void*) line);

   return(res);
}


/* ========================================================================== */
/* Export configuration */

static int  conf_export_configuration(struct conf*  cfg,
                                      FILE*  fs, FILE*  fs_tmp)
{
   int  res = -1;
   char*  line = NULL;
   size_t  len = 0;
   posix_ssize_t  readlen;
   int  rv;
   unsigned int  i;
   int  reqd;
   char*  p;

   while(1)
   {
      /* Read line */
      readlen = posix_getline(&line, &len, fs);
      if(-1 == readlen)
      {
         if(POSIX_ENOMEM == posix_errno)
         {
            print_error("CFG: Cannot assign memory for config file parser");
         }
         else
         {
            /* Check for error */
            if(ferror(fs))
            {
               print_error("CFG: Parse error in config file");
               break;
            }
            /* Check for EOF */
            if(feof(fs))
            {
               res = 0;
               break;
            }
         }
      }
      if(0 >= readlen)  { break; }
      else
      {
         /* Update data */
         for(i = 0; i < CONF_NUM; ++i)
         {
            if(!conf_update_entry(&line, len, &cfg[i]))  { break; }
         }

         /* Write line to new config file */
         rv = fprintf(fs_tmp, "%s", line);
         if(0 > rv)  { break; }
      }
   }

   /* Add missing entries to end of config file */
   if(!res)
   {
      for(i = 0; i < CONF_NUM; ++i)
      {
         if(!cfg[i].found)
         {
            /* Create new entry */
            reqd = strlen(cfg[i].label);
            if(POSIX_SIZE_MAX <= reqd)  { break; }
            if(reqd + 1 > len)
            {
               /* Allocate more memory */
               p = (char*) posix_realloc(line, reqd + 1);
               if (NULL != p)  line = p;
            }
            strncpy(line, cfg[i].label, len);
            rv = conf_update_entry(&line, len, &cfg[i]);
            /* Write new entry */
            if(!rv)
            {
               rv = fprintf(fs_tmp, "%s", line);
               if(0 > rv)
               {
                  res = -1;
                  break;
               }
            }
         }
      }
   }

   /* Release memory for line buffer */
   posix_free((void*) line);

   return(res);
}


/* ========================================================================== */
/*! \brief Delete configuration 
 *
 * This function will free the memory allocated for the configuration array
 * entries with string type.
 *
 * \param[in] cfg  Pointer to configuration array
 */

void  conf_delete(struct conf*  cfg)
{
   unsigned int  i;

   if(conf_initialized)
   {
      for(i = 0; i < CONF_NUM; ++i)
      {
         if(CONF_TYPE_STRING == cfg[i].type)
         {
            posix_free((void*) cfg[i].val.s);
         }
      }
   }
}


/* ========================================================================== */
/*! \brief Load configuration from config file
 *
 * \param[in] cfg  Pointer to configuration array
 *
 * For configuration entries with string type, this function allocates memory.
 * The caller is responsible to free this memory with the function
 * \ref conf_delete() .
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  conf_load(struct conf*  cfg)
{
   int  res = -1;
   int  fd = -1;
   FILE*  fs = NULL;
   char*  confpathname = NULL;

   /* Init configuration with default values */
   conf_delete(cfg);
   conf_init_configuration(cfg);
   if(conf_initialized)
   {
      /* Get config file pathname */
      res = conf_get_confpathname(&confpathname, confname);
      if(!res)
      {
         /* Open and lock config file */
         res = fu_open_file(confpathname, &fd, POSIX_O_RDWR | POSIX_O_CREAT,
                            CONF_PERM);
         if(!res)  { res = fu_lock_file(fd); }
         if(!res)  { res = fu_assign_stream(fd, &fs, "r"); }
      }

      /* Import configuration from config file */
      if(!res)  { res = conf_import_configuration(cfg, fs); }

      /* Print error message if required */
      if(res)
      {
         print_error("CFG: Loading configuration failed, using defaults");
      }

      /* Close config file (this will also release the lock) */
      fu_close_file(&fd, &fs);

      /* Release memory for pathname */
      posix_free((void*) confpathname);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Store configuration to config file
 *
 * \param[in] cfg  Pointer to configuration array
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  conf_store(struct conf*  cfg)
{
   int  res = -1;
   int  fd = -1;
   FILE*  fs = NULL;
   int  fd_tmp = -1;
   FILE*  fs_tmp = NULL;
   char*  confpathname = NULL;
   char*  tmppathname = NULL;

   /* Get file pathnames */
   res = conf_get_confpathname(&confpathname, confname);
   if(!res)  { res = conf_get_confpathname(&tmppathname, tmpname); }

   /* Open and lock config file */
   if(!res)
   {
      res = fu_open_file(confpathname, &fd, POSIX_O_RDWR | POSIX_O_CREAT,
                         CONF_PERM);
      if(!res)  { res = fu_lock_file(fd); }
      if(!res)  { res = fu_assign_stream(fd, &fs, "r"); }
   }

   /* Open temporary file for new configuration */
   if(!res)
   {
      res = fu_open_file(tmppathname, &fd_tmp,
                         POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC,
                         CONF_PERM);
      /*
       * Because we have the lock for the config file, it is allowed to
       * assume that no other instance of the program currently use the
       * temporary filename.
       */
      if(!res)  { res = fu_assign_stream(fd_tmp, &fs_tmp, "w"); }
   }

   /* Export configuration to temporary file */
   if(!res)
   {
      if(conf_ephemeral_passwd)
      {
         /* Delete password from configuration */
         conf_string_replace(&config[CONF_PASS], "");
      }
      res = conf_export_configuration(cfg, fs, fs_tmp);
   }

   /* Flush stream of temporary file*/
   if (!res)  { fu_sync(fd_tmp, fs_tmp); }

   /* Rename temporary file to config file */
   if(!res)  { res = posix_rename(tmppathname, confpathname); }

   /* Unlink temporary file after error */
   if(res)
   {
      print_error("CFG: Failed to store configuration in config file");
      if(tmppathname)  { fu_unlink_file(tmppathname); }
   }

   /* The tmp file must be removed before releasing the config file lock! */
   fu_close_file(&fd_tmp, &fs_tmp);

   /* Close config file (this will also release the lock) */
   fu_close_file(&fd, &fs);

   /* Release memory for file pathnames */
   posix_free((void*) tmppathname);
   posix_free((void*) confpathname);

   return(res);
}


/* ========================================================================== */
/*! \brief Replace configuration string
 *
 * \param[in] cfg  Pointer to configuration array entry
 * \param[in] s    Pointer to new string
 *
 * \e cfg must point to a configuration entry of string type.
 * The current string will be replaced with \e s .
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  conf_string_replace(struct conf*  cfg, const char*  s)
{
   int  res = -1;
   size_t  len;
   char*  p;

   if(NULL != cfg && NULL != s)
   {
      if(CONF_TYPE_STRING == cfg->type)
      {
         len = strlen(s);
         p = posix_realloc((void*) cfg->val.s, ++len);
         if(NULL != p)
         {
            strcpy(p , s);
            cfg->val.s = p;
         }
      }
   }

   return(res);
}


/* @} */

/* EOF */
