/* ========================================================================== */
/*! \file
 * \brief National language support (NLS)
 *
 * Copyright (c) 2012-2021 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 beause of feature test macros */

#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "config.h"
#include "main.h"
#include "nls.h"


/* ========================================================================== */
/*! \defgroup NLS NLS: National Language Support
 *
 * This implementation of NLS does not use GNU gettext. The reasons are:
 * - No additional library dependency for stuff that is already in libc
 * - No string searching via compare, hash or other algorithm at runtime
 *   (Strings are not searched at all but instead directly accessed via index)
 *
 * This should waste minimum ressources and give best runtime performance.
 * All the expensive work is done only once at build time.
 */
/*! @{ */


/* ========================================================================== */
/* Data types */

enum nls_locale
{
   NLS_LOCALE_VALID,
   NLS_LOCALE_DEFAULT,
   NLS_LOCALE_INVALID
};


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

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

/*! \brief Maximum number of messages loaded from NLS catalog.
 *
 * This value must at least match the number of messages in the NLS catalogs
 * used by the program.
 * The limit of this implementation is INT_MAX. If the system has a lower
 * limit than the configured value, the value is clamped to the system limit.
 *
 * \warning
 * Making this value much higher than required will waste memory and slow
 * down startup.
 */
#define NLS_MAX_MESSAGE  384


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

/*! \brief Current NLS locale */
char  nls_loc[6] = "";
#if CFG_USE_XSI && !CFG_NLS_DISABLE
static int  nls_ready = 0;
static int  nls_last_message = 0;
static const char**  nls_message = NULL;
#endif  /* CFG_USE_XSI && !CFG_NLS_DISABLE */


/* ========================================================================== */
/* Copy NLS catalog to local memory */
/*
 * The internal buffer of \c catgets() may get overwritten by the next call,
 * this is the case e.g. on HP-UX. POSIX.1 doesn't explicitly forbid this
 * behaviour and it must therefore considered as legal.
 *
 * Because we need NLS strings that behave like 'static const char' arrays
 * until \ref nls_exit() is called, we copy the whole catalog to local memory
 * and work with the copy.
 */

#if CFG_USE_XSI && !CFG_NLS_DISABLE
static int  nls_import_catalog(posix_nl_catd  catd)
{
   int  res = 0;
   size_t  i;
   char*  rv;
   int  nls_max_message = NLS_MAX_MESSAGE;
   size_t  len;
   const char**  array;
   const char*  s;

   /* Clamp maximum number of messages to system limit */
   if(POSIX_NL_MSGMAX < nls_max_message)  { nls_max_message = POSIX_NL_MSGMAX; }

   /* Message number zero is reserved */
   for(i = 1; i <= (size_t) nls_max_message; ++i)
   {
      do
      {
         posix_errno = 0;
         rv = posix_catgets(catd, 1, (int) i, NULL);
      }
      while(NULL == rv && POSIX_EINTR == posix_errno);
      len = sizeof(const char*) * i;
      array = (const char**) posix_realloc(nls_message, len);
      if(NULL == array)
      {
         PRINT_ERROR("Out of memory while creating message array");
         res = -1;
         break;
      }
      else
      {
         nls_message = array;
         nls_last_message = (int) i;
         s = NULL;
         if(rv)
         {
            /* Copy string */
            len = strlen(rv) + (size_t) 1;
            s = (const char*) posix_malloc(len);
            if(s)  { memcpy((void*) s, (void*) rv, len); }
         }
         nls_message[i - (size_t) 1] = s;
      }
   }

   return(res);
}
#endif  /* CFG_USE_XSI && !CFG_NLS_DISABLE */


/* ========================================================================== */
/*! \brief Init NLS subsystem
 *
 * First the locale of the program is set. If this fails the locale \c POSIX is
 * used. The locale \c C is also mapped to \c POSIX and no NLS catalog is used
 * for the locale \c POSIX.
 * Then we try to extract the language code \c xx and the country code \c YY
 * of the message locale name from \c LC_MESSAGES. If the locale has no country
 * code (old 2 digit style), \c XX is used.
 * If a valid locale was found, a catalog with the name \c xx_YY.cat is searched
 * in the location \c CFG_NLS_PATH that was compiled into the program.
 *
 * \attention
 * The environment variable \c NLSPATH is ignored for security reasons. The
 * administrator who installed the program should have control over the NLS
 * strings that are processed by the program. Think twice before changing this
 * because we use Unicode NLS catalogs and the validity of the encoding must be
 * verfied at runtime otherwise.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */


int  nls_init(void)
{
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   int  res = 0;
   const char*  loc_sys;
   enum nls_locale  rv = NLS_LOCALE_VALID;
   posix_nl_catd  catd = (posix_nl_catd) -1;
   int  rv2;
   size_t  len;
   long int  len_max;
   const char*  subdir = "/";
   const char*  catext = ".cat";
   char*  catname = NULL;

   /* Check whether we are already initialized */
   if(nls_ready)
   {
      PRINT_ERROR("Subsystem is already initialized");
      res = -1;
   }

   /* Set all locale categories from environment */
   if(!res)
   {
      loc_sys = posix_setlocale(POSIX_LC_ALL, "");
      if(NULL == loc_sys)
      {
         /* Failed => Fall back to POSIX */
         PRINT_ERROR("Cannot set locale (check 'LANG' and 'LC_*' variables)");
         PRINT_ERROR("Using default POSIX locale");
         strcpy(nls_loc, "POSIX");
         res = -1;
      }
   }

   /* Get message locale */
   if(!res)
   {
      /* Get raw value of locale category 'LC_MESSAGES' */
      loc_sys = posix_setlocale(POSIX_LC_MESSAGES, "");
      printf("%s: %sMessage locale: %s\n", CFG_NAME, MAIN_ERR_PREFIX, loc_sys);
      /* Cook internal locale from first 5 characters */
      strncpy(nls_loc, loc_sys, 5);  nls_loc[5] = 0;
      /* Check for POSIX and C locale */
      if(!strcmp(nls_loc, "C") || !strcmp(nls_loc, "POSIX"))
      {
         strcpy(nls_loc, "POSIX");
         rv = NLS_LOCALE_DEFAULT;
      }
      /* Check whether locale is in "*_*" format */
      else if('_' == nls_loc[2])
      {
         /* Yes => Try to convert it to "xx_YY" */
         if(!islower((unsigned char) nls_loc[0]))
         {
            if(isupper((unsigned char) nls_loc[0]))
            {
               nls_loc[0] = (char) tolower((unsigned char) nls_loc[0]);
            }
            else  rv = NLS_LOCALE_INVALID;
         }
         if(!islower((unsigned char) nls_loc[1]))
         {
            if (isupper((unsigned char) nls_loc[1]))
            {
               nls_loc[1] = (char) tolower((unsigned char) nls_loc[1]);
            }
            else  rv = NLS_LOCALE_INVALID;
         }
         if(!isupper((unsigned char) nls_loc[3]))
         {
            if(islower((unsigned char) nls_loc[3]))
            {
               nls_loc[3] = (char) toupper((unsigned char) nls_loc[3]);
            }
            else  rv = NLS_LOCALE_INVALID;
         }
         if(!isupper((unsigned char) nls_loc[4]))
         {
            if(islower((unsigned char) nls_loc[4]))
            {
               nls_loc[4] = (char) toupper((unsigned char) nls_loc[4]);
            }
            else  rv = NLS_LOCALE_INVALID;
         }
      }
      /* Check for old "xx" format */
      else if(2 == strlen(nls_loc) || ('.' == nls_loc[2]))
      {
         /* Yes => Try to convert it to "xx_YY" */
         if(!islower((unsigned char) nls_loc[0]))
         {
            if(isupper((unsigned char) nls_loc[0]))
            {
               nls_loc[0] = (char) tolower((unsigned char) nls_loc[0]);
            }
            else  rv = NLS_LOCALE_INVALID;
         }
         if(!islower((unsigned char) nls_loc[1]))
         {
            if (isupper((unsigned char) nls_loc[1]))
            {
               nls_loc[1] = (char) tolower((unsigned char) nls_loc[1]);
            }
            else  rv = NLS_LOCALE_INVALID;
         }
         if(NLS_LOCALE_VALID == rv)
         {
            nls_loc[2] = '_';
            nls_loc[3] = (char) toupper((unsigned char) nls_loc[0]);
            nls_loc[4] = (char) toupper((unsigned char) nls_loc[1]);
            nls_loc[5] = 0;
         }
      }
      else  rv = NLS_LOCALE_INVALID;
      /* Check whether we have a valid locale */
      switch(rv)
      {
         case NLS_LOCALE_INVALID:
         {
            /* Print error if locale format is not supported */
            PRINT_ERROR("Locale name format not supported");
            PRINT_ERROR("Format must be 'xx' or start with 'xx_YY'");
            res = -1;
            break;
         }
         case NLS_LOCALE_VALID:
         case NLS_LOCALE_DEFAULT:
         {
            printf("%s: %sCooked message locale: %s\n",
                   CFG_NAME, MAIN_ERR_PREFIX, nls_loc);
            if(NLS_LOCALE_DEFAULT == rv)
            {
               /* Return error to use default strings instead of NLS catalog */
               printf("%s: %sNo catalog required\n", CFG_NAME, MAIN_ERR_PREFIX);
               res = -1;
            }
            break;
         }
         default:
         {
            PRINT_ERROR("Error while processing locale");
            res = -1;
            break;
         }
      }
   }

   /* Allocate memory and prepare NLS catalog pathname */
   if(!res)
   {
      /* Calculate pathname length (including termination character) */
      len = strlen(CFG_NLS_PATH);
      len += strlen(subdir);
      len += strlen(nls_loc);
      len += strlen(catext);
      ++len;
      /* Allocate memory for NLS catalog pathname */
      catname = posix_malloc(len);
      if(NULL == catname)
      {
         PRINT_ERROR("Out of memory while creating catalog pathname");
         res = -1;
      }
      else
      {
         strcpy(catname, CFG_NLS_PATH);
         strcat(catname, subdir);
         strcat(catname, nls_loc);
         strcat(catname, catext);
         printf("%s: %sUsing catalog: %s\n", CFG_NAME, MAIN_ERR_PREFIX,
                catname);
      }
      /* Check whether system supports path length */
      len_max = posix_pathconf(CFG_NLS_PATH, POSIX_PC_PATH_MAX);
      if(0L > len_max)
      {
         /* Path length check failed */
         PRINT_ERROR("Catalog path check failed");
         res = -1;
      }
      else if((size_t) len_max < len)
      {
         /* Pathname too long (not supported by OS) */
         PRINT_ERROR("Catalog pathname too long");
         res = -1;
      }
   }

   /* Open NLS catalog */
   if(!res)
   {
      /*
       * Workaround for old GNU systems:
       * They report a positive catalog descriptor even if there is no catalog.
       * As an additional check we open the catalog with 'open()' to check
       * whether it is present.
       */
      rv2 = posix_open(catname, POSIX_O_RDONLY);
      if(-1 == rv2)
      {
         PRINT_ERROR("Catalog not found, using default strings");
         res = -1;
      }
      else
      {
         posix_close(rv2);
         catd = posix_catopen(catname, POSIX_NL_CAT_LOCALE);
         if((posix_nl_catd) -1 == catd)
         {
            switch(posix_errno)
            {
               case POSIX_EACCES:
               {
                  PRINT_ERROR("Permission denied to open catalog");
                  break;
               }
               case POSIX_ENOENT:
               {
                  PRINT_ERROR("Catalog not found");
                  break;
               }
               case POSIX_ENOMEM:
               {
                  PRINT_ERROR("Out of memory while opening catalog");
                  break;
               }
               default:
               {
                  PRINT_ERROR("Failed to open catalog");
                  break;
               }
            }
            res = -1;
         }
      }
   }

   /* Release memory for NLS catalog pathname */
   posix_free(catname);

   if(!res)
   {
      /* Import NLS catalog data to local buffer */
      res = nls_import_catalog(catd);

      /* Close NLS catalog */
      do
      {
         posix_errno = 0;
         rv2 = posix_catclose(catd);
      }
      while(-1 == rv2 && POSIX_EINTR == posix_errno);
   }

   nls_ready = !res;

   return(res);
#else  /* CFG_USE_XSI && !CFG_NLS_DISABLE */
   strcpy(nls_loc, "POSIX");
   PRINT_ERROR("Disabled by configuration");

   return(-1);
#endif  /* CFG_USE_XSI && !CFG_NLS_DISABLE */
}


/* ========================================================================== */
/*! \brief Shutdown NLS subsystem
 *
 * It is allowed to call this function even if \ref nls_init() have failed.
 * In this case it simply does nothing.
 */

void  nls_exit(void)
{
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   size_t  i;

   if(nls_ready && nls_last_message)
   {
      for(i = 0; i < (size_t) nls_last_message; ++i)
      {
         posix_free((void*) nls_message[i]);
      }
      posix_free((void*) nls_message);
   }

   nls_ready = 0;
#endif  /* CFG_USE_XSI && !CFG_NLS_DISABLE */
}


/* ========================================================================== */
/*! \brief Get NLS string
 *
 * \param[in] n  NLS string number
 * \param[in] s  Replacement string if translation for string \e n was not found
 *
 * It is allowed to call this function even if nls_init() have failed or after
 * \ref nls_exit() . In this case always \e s is returned.
 *
 * \return
 * This functions returns the message string number \e n (or the default \e s
 * if no corresponding message is found in the catalog)
 */

const char*  nls_getstring(int  n, const char*  s)
{
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   const char*  res = s;

   if(nls_ready)
   {
      if(1 > n || n > nls_last_message)
      {
         PRINT_ERROR("Value of NLS_MAX_MESSAGE too low (Bug)");
      }
      else
      {
         /* Decrement index (message number zero doesn't exist in array) */
         if(nls_message[--n])  { res = nls_message[n]; }
      }
      if(res == s)
      {
         PRINT_ERROR("Translation not found in catalog");
      }
   }

   return(res);
#else  /* CFG_USE_XSI && !CFG_NLS_DISABLE */
   return(s);
#endif  /* CFG_USE_XSI && !CFG_NLS_DISABLE */
}


/*! @} */

/* EOF */
