// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// locale.c: functions for language/locale configuration

#include <stdbool.h>
#include <stdio.h>

#include "auto/config.h"
#ifdef HAVE_LOCALE_H
# include <locale.h>
#endif

#include "nvim/ascii.h"
#include "nvim/buffer.h"
#include "nvim/charset.h"
#include "nvim/eval.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/garray.h"
#include "nvim/gettext.h"
#include "nvim/locale.h"
#include "nvim/macros.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/option.h"
#include "nvim/os/os.h"
#include "nvim/os/shell.h"
#include "nvim/path.h"
#include "nvim/profile.h"
#include "nvim/types.h"
#include "nvim/vim.h"

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "locale.c.generated.h"
#endif

#if defined(HAVE_LOCALE_H)
# define HAVE_GET_LOCALE_VAL

static char *get_locale_val(int what)
{
  // Obtain the locale value from the libraries.
  char *loc = setlocale(what, NULL);

  return loc;
}
#endif

/// @return  true when "lang" starts with a valid language name.
///          Rejects NULL, empty string, "C", "C.UTF-8" and others.
static bool is_valid_mess_lang(const char *lang)
{
  return lang != NULL && ASCII_ISALPHA(lang[0]) && ASCII_ISALPHA(lang[1]);
}

/// Obtain the current messages language.  Used to set the default for
/// 'helplang'.  May return NULL or an empty string.
char *get_mess_lang(void)
{
  char *p;

#ifdef HAVE_GET_LOCALE_VAL
# if defined(LC_MESSAGES)
  p = get_locale_val(LC_MESSAGES);
# else
  // This is necessary for Win32, where LC_MESSAGES is not defined and $LANG
  // may be set to the LCID number.  LC_COLLATE is the best guess, LC_TIME
  // and LC_MONETARY may be set differently for a Japanese working in the
  // US.
  p = get_locale_val(LC_COLLATE);
# endif
#else
  p = os_getenv("LC_ALL");
  if (!is_valid_mess_lang(p)) {
    p = os_getenv("LC_MESSAGES");
    if (!is_valid_mess_lang(p)) {
      p = os_getenv("LANG");
    }
  }
#endif
  return is_valid_mess_lang(p) ? p : NULL;
}

// Complicated #if; matches with where get_mess_env() is used below.
#ifdef HAVE_WORKING_LIBINTL
/// Get the language used for messages from the environment.
static char *get_mess_env(void)
{
  char *p;

  p = (char *)os_getenv("LC_ALL");
  if (p == NULL) {
    p = (char *)os_getenv("LC_MESSAGES");
    if (p == NULL) {
      p = (char *)os_getenv("LANG");
      if (p != NULL && ascii_isdigit(*p)) {
        p = NULL;                       // ignore something like "1043"
      }
# ifdef HAVE_GET_LOCALE_VAL
      if (p == NULL) {
        p = get_locale_val(LC_CTYPE);
      }
# endif
    }
  }
  return p;
}
#endif

/// Set the "v:lang" variable according to the current locale setting.
/// Also do "v:lc_time"and "v:ctype".
void set_lang_var(void)
{
  const char *loc;

#ifdef HAVE_GET_LOCALE_VAL
  loc = get_locale_val(LC_CTYPE);
#else
  // setlocale() not supported: use the default value
  loc = "C";
#endif
  set_vim_var_string(VV_CTYPE, loc, -1);

  // When LC_MESSAGES isn't defined use the value from $LC_MESSAGES, fall
  // back to LC_CTYPE if it's empty.
#ifdef HAVE_WORKING_LIBINTL
  loc = get_mess_env();
#elif defined(LC_MESSAGES)
  loc = get_locale_val(LC_MESSAGES);
#else
  // In Windows LC_MESSAGES is not defined fallback to LC_CTYPE
  loc = get_locale_val(LC_CTYPE);
#endif
  set_vim_var_string(VV_LANG, loc, -1);

#ifdef HAVE_GET_LOCALE_VAL
  loc = get_locale_val(LC_TIME);
#endif
  set_vim_var_string(VV_LC_TIME, loc, -1);

#ifdef HAVE_GET_LOCALE_VAL
  loc = get_locale_val(LC_COLLATE);
#else
  // setlocale() not supported: use the default value
  loc = "C";
#endif
  set_vim_var_string(VV_COLLATE, loc, -1);
}

#if defined(HAVE_LOCALE_H)
/// Setup to use the current locale (for ctype() and many other things).
void init_locale(void)
{
  setlocale(LC_ALL, "");

# ifdef LC_NUMERIC
  // Make sure strtod() uses a decimal point, not a comma.
  setlocale(LC_NUMERIC, "C");
# endif

  char localepath[MAXPATHL] = { 0 };
  snprintf(localepath, sizeof(localepath), "%s", get_vim_var_str(VV_PROGPATH));
  char *tail = path_tail_with_sep(localepath);
  *tail = NUL;
  tail = path_tail(localepath);
  xstrlcpy(tail, "share/locale",
           sizeof(localepath) - (size_t)(tail - localepath));
  bindtextdomain(PROJECT_NAME, localepath);
  textdomain(PROJECT_NAME);
  TIME_MSG("locale set");
}
#endif

#ifdef HAVE_WORKING_LIBINTL

/// ":language":  Set the language (locale).
///
/// @param eap
void ex_language(exarg_T *eap)
{
  char *loc;
  char *p;
  char *name;
  int what = LC_ALL;
  char *whatstr = "";
# ifdef LC_MESSAGES
#  define VIM_LC_MESSAGES LC_MESSAGES
# else
#  define VIM_LC_MESSAGES 6789
# endif

  name = eap->arg;

  // Check for "messages {name}", "ctype {name}" or "time {name}" argument.
  // Allow abbreviation, but require at least 3 characters to avoid
  // confusion with a two letter language name "me" or "ct".
  p = skiptowhite(eap->arg);
  if ((*p == NUL || ascii_iswhite(*p)) && p - eap->arg >= 3) {
    if (STRNICMP(eap->arg, "messages", p - eap->arg) == 0) {
      what = VIM_LC_MESSAGES;
      name = skipwhite(p);
      whatstr = "messages ";
    } else if (STRNICMP(eap->arg, "ctype", p - eap->arg) == 0) {
      what = LC_CTYPE;
      name = skipwhite(p);
      whatstr = "ctype ";
    } else if (STRNICMP(eap->arg, "time", p - eap->arg) == 0) {
      what = LC_TIME;
      name = skipwhite(p);
      whatstr = "time ";
    } else if (STRNICMP(eap->arg, "collate", p - eap->arg) == 0) {
      what = LC_COLLATE;
      name = skipwhite(p);
      whatstr = "collate ";
    }
  }

  if (*name == NUL) {
    if (what == VIM_LC_MESSAGES) {
      p = get_mess_env();
    } else {
      p = setlocale(what, NULL);
    }
    if (p == NULL || *p == NUL) {
      p = "Unknown";
    }
    smsg(_("Current %slanguage: \"%s\""), whatstr, p);
  } else {
# ifndef LC_MESSAGES
    if (what == VIM_LC_MESSAGES) {
      loc = "";
    } else {
# endif
    loc = setlocale(what, name);
# ifdef LC_NUMERIC
    // Make sure strtod() uses a decimal point, not a comma.
    setlocale(LC_NUMERIC, "C");
# endif
# ifndef LC_MESSAGES
  }
# endif
    if (loc == NULL) {
      semsg(_("E197: Cannot set language to \"%s\""), name);
    } else {
# ifdef HAVE_NL_MSG_CAT_CNTR
      // Need to do this for GNU gettext, otherwise cached translations
      // will be used again.
      extern int _nl_msg_cat_cntr;

      _nl_msg_cat_cntr++;
# endif
      // Reset $LC_ALL, otherwise it would overrule everything.
      os_setenv("LC_ALL", "", 1);

      if (what != LC_TIME && what != LC_COLLATE) {
        // Tell gettext() what to translate to.  It apparently doesn't
        // use the currently effective locale.
        if (what == LC_ALL) {
          os_setenv("LANG", name, 1);

          // Clear $LANGUAGE because GNU gettext uses it.
          os_setenv("LANGUAGE", "", 1);
        }
        if (what != LC_CTYPE) {
          os_setenv("LC_MESSAGES", name, 1);
          set_helplang_default(name);
        }
      }

      // Set v:lang, v:lc_time, v:collate and v:ctype to the final result.
      set_lang_var();
      maketitle();
    }
  }
}

static char **locales = NULL;       // Array of all available locales

# ifndef MSWIN
static bool did_init_locales = false;

/// @return  an array of strings for all available locales + NULL for the
///          last element or,
///          NULL in case of error.
static char **find_locales(void)
{
  garray_T locales_ga;
  char *loc;
  char *saveptr = NULL;

  // Find all available locales by running command "locale -a".  If this
  // doesn't work we won't have completion.
  char *locale_a = (char *)get_cmd_output((char_u *)"locale -a", NULL,
                                          kShellOptSilent, NULL);
  if (locale_a == NULL) {
    return NULL;
  }
  ga_init(&locales_ga, sizeof(char_u *), 20);

  // Transform locale_a string where each locale is separated by "\n"
  // into an array of locale strings.
  loc = os_strtok(locale_a, "\n", &saveptr);

  while (loc != NULL) {
    loc = xstrdup(loc);
    GA_APPEND(char *, &locales_ga, loc);
    loc = os_strtok(NULL, "\n", &saveptr);
  }
  xfree(locale_a);
  // Guarantee that .ga_data is NULL terminated
  ga_grow(&locales_ga, 1);
  ((char_u **)locales_ga.ga_data)[locales_ga.ga_len] = NULL;
  return locales_ga.ga_data;
}
# endif

/// Lazy initialization of all available locales.
static void init_locales(void)
{
# ifndef MSWIN
  if (!did_init_locales) {
    did_init_locales = true;
    locales = find_locales();
  }
# endif
}

# if defined(EXITFREE)
void free_locales(void)
{
  int i;
  if (locales != NULL) {
    for (i = 0; locales[i] != NULL; i++) {
      xfree(locales[i]);
    }
    XFREE_CLEAR(locales);
  }
}
# endif

/// Function given to ExpandGeneric() to obtain the possible arguments of the
/// ":language" command.
char *get_lang_arg(expand_T *xp, int idx)
{
  if (idx == 0) {
    return "messages";
  }
  if (idx == 1) {
    return "ctype";
  }
  if (idx == 2) {
    return "time";
  }
  if (idx == 3) {
    return "collate";
  }

  init_locales();
  if (locales == NULL) {
    return NULL;
  }
  return locales[idx - 4];
}

/// Function given to ExpandGeneric() to obtain the available locales.
char *get_locales(expand_T *xp, int idx)
{
  init_locales();
  if (locales == NULL) {
    return NULL;
  }
  return locales[idx];
}

#endif
