/* Determine the user's language preferences.
   Copyright (C) 2004-2007, 2018-2023 Free Software Foundation, Inc.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as published by
   the Free Software Foundation; either version 2.1 of the License, or
   (at your option) any later version.

   This program 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 Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

/* Written by Bruno Haible <bruno@clisp.org>.
   Win32 code originally by Michele Cicciotti <hackbunny@reactos.com>.  */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

/* Specification.  */
#include "gettextP.h"

#include <stdlib.h>

#if HAVE_CFLOCALECOPYPREFERREDLANGUAGES || HAVE_CFPREFERENCESCOPYAPPVALUE
# include <string.h>
# if HAVE_CFLOCALECOPYPREFERREDLANGUAGES
#  include <CoreFoundation/CFLocale.h>
# elif HAVE_CFPREFERENCESCOPYAPPVALUE
#  include <CoreFoundation/CFPreferences.h>
# endif
# include <CoreFoundation/CFPropertyList.h>
# include <CoreFoundation/CFArray.h>
# include <CoreFoundation/CFString.h>
extern void gl_locale_name_canonicalize (char *name);
#endif

#if defined _WIN32
# define WIN32_NATIVE
#endif

#ifdef WIN32_NATIVE
# define WIN32_LEAN_AND_MEAN
# include <windows.h>

# ifndef MUI_LANGUAGE_NAME
# define MUI_LANGUAGE_NAME 8
# endif
# ifndef STATUS_BUFFER_OVERFLOW
# define STATUS_BUFFER_OVERFLOW 0x80000005
# endif

extern void gl_locale_name_canonicalize (char *name);
extern const char *gl_locale_name_from_win32_LANGID (LANGID langid);
extern const char *gl_locale_name_from_win32_LCID (LCID lcid);

/* Get the preferences list through the MUI APIs. This works on Windows Vista
   and newer.  */
static const char *
_nl_language_preferences_win32_mui (HMODULE kernel32)
{
  /* DWORD GetUserPreferredUILanguages (ULONG dwFlags,
                                        PULONG pulNumLanguages,
                                        PWSTR pwszLanguagesBuffer,
                                        PULONG pcchLanguagesBuffer);  */
  typedef DWORD (WINAPI *GetUserPreferredUILanguages_func) (ULONG, PULONG, PWSTR, PULONG);
  GetUserPreferredUILanguages_func p_GetUserPreferredUILanguages;

  p_GetUserPreferredUILanguages =
   (GetUserPreferredUILanguages_func)
   GetProcAddress (kernel32, "GetUserPreferredUILanguages");
  if (p_GetUserPreferredUILanguages != NULL)
    {
      ULONG num_languages;
      ULONG bufsize;
      DWORD ret;

      bufsize = 0;
      ret = p_GetUserPreferredUILanguages (MUI_LANGUAGE_NAME,
                                           &num_languages,
                                           NULL, &bufsize);
      if (ret == 0
          && GetLastError () == STATUS_BUFFER_OVERFLOW
          && bufsize > 0)
        {
          WCHAR *buffer = (WCHAR *) malloc (bufsize * sizeof (WCHAR));
          if (buffer != NULL)
            {
              ret = p_GetUserPreferredUILanguages (MUI_LANGUAGE_NAME,
                                                   &num_languages,
                                                   buffer, &bufsize);
              if (ret)
                {
                  /* Convert the list from NUL-delimited WCHAR[] Win32 locale
                     names to colon-delimited char[] Unix locale names.
                     We assume that all these locale names are in ASCII,
                     nonempty and contain no colons.  */
                  char *languages =
                    (char *) malloc (bufsize + num_languages * 10 + 1);
                  if (languages != NULL)
                    {
                      const WCHAR *p = buffer;
                      char *q = languages;
                      ULONG i;
                      for (i = 0; i < num_languages; i++)
                        {
                          char *q1;
                          char *q2;

                          q1 = q;
                          if (i > 0)
                            *q++ = ':';
                          q2 = q;
                          for (; *p != (WCHAR)'\0'; p++)
                            {
                              if ((unsigned char) *p != *p || *p == ':')
                                {
                                  /* A non-ASCII character or a colon inside
                                     the Win32 locale name! Punt.  */
                                  q = q1;
                                  break;
                                }
                              *q++ = (unsigned char) *p;
                            }
                          if (q == q1)
                            /* An unexpected Win32 locale name occurred.  */
                            break;
                          *q = '\0';
                          gl_locale_name_canonicalize (q2);
                          q = q2 + strlen (q2);
                          p++;
                        }
                      *q = '\0';
                      if (q > languages)
                        {
                          free (buffer);
                          return languages;
                        }
                      free (languages);
                    }
                }
              free (buffer);
            }
        }
    }
  return NULL;
}

/* Get a preference.  This works on Windows ME and newer.  */
static const char *
_nl_language_preferences_win32_ME (HMODULE kernel32)
{
  /* LANGID GetUserDefaultUILanguage (void);  */
  typedef LANGID (WINAPI *GetUserDefaultUILanguage_func) (void);
  GetUserDefaultUILanguage_func p_GetUserDefaultUILanguage;

  p_GetUserDefaultUILanguage =
   (GetUserDefaultUILanguage_func)
   GetProcAddress (kernel32, "GetUserDefaultUILanguage");
  if (p_GetUserDefaultUILanguage != NULL)
    return gl_locale_name_from_win32_LANGID (p_GetUserDefaultUILanguage ());
  return NULL;
}

/* Get a preference.  This works on Windows 95 and newer.  */
static const char *
_nl_language_preferences_win32_95 ()
{
  HKEY desktop_resource_locale_key;

  if (RegOpenKeyExA (HKEY_CURRENT_USER,
                     "Control Panel\\Desktop\\ResourceLocale",
                     0, KEY_QUERY_VALUE, &desktop_resource_locale_key)
      == NO_ERROR)
    {
      DWORD type;
      BYTE data[8 + 1];
      DWORD data_size = sizeof (data);
      DWORD ret;

      ret = RegQueryValueExA (desktop_resource_locale_key, NULL, NULL,
                              &type, data, &data_size);
      RegCloseKey (desktop_resource_locale_key);

      if (ret == NO_ERROR)
        {
          /* We expect a string, at most 8 bytes long, that parses as a
             hexadecimal number.  */
          if (type == REG_SZ
              && data_size <= sizeof (data)
              && (data_size < sizeof (data)
                  || data[sizeof (data) - 1] == '\0'))
            {
              LCID lcid;
              char *endp;
              /* Ensure it's NUL terminated.  */
              if (data_size < sizeof (data))
                data[data_size] = '\0';
              /* Parse it as a hexadecimal number.  */
              lcid = strtoul ((char *) data, &endp, 16);
              if (endp > (char *) data && *endp == '\0')
                return gl_locale_name_from_win32_LCID (lcid);
            }
        }
    }
  return NULL;
}

/* Get the system's preference.  This can be used as a fallback.  */
static BOOL CALLBACK
ret_first_language (HMODULE h, LPCSTR type, LPCSTR name, WORD lang, LONG_PTR param)
{
  *(const char **)param = gl_locale_name_from_win32_LANGID (lang);
  return FALSE;
}
static const char *
_nl_language_preferences_win32_system (HMODULE kernel32)
{
  const char *languages = NULL;
  /* Ignore the warning on mingw here. mingw has a wrong definition of the last
     parameter type of ENUMRESLANGPROC.  */
  EnumResourceLanguages (kernel32, RT_VERSION, MAKEINTRESOURCE (1),
                         ret_first_language, (LONG_PTR)&languages);
  return languages;
}

#endif

/* Determine the user's language preferences, as a colon separated list of
   locale names in XPG syntax
     language[_territory][.codeset][@modifier]
   The result must not be freed; it is statically allocated.
   The LANGUAGE environment variable does not need to be considered; it is
   already taken into account by the caller.  */

const char *
_nl_language_preferences_default (void)
{
#if HAVE_CFLOCALECOPYPREFERREDLANGUAGES || HAVE_CFPREFERENCESCOPYAPPVALUE
  /* MacOS X 10.4 or newer */
  {
    /* Cache the preferences list, since CoreFoundation calls are expensive.  */
    static const char *cached_languages;
    static int cache_initialized;

    if (!cache_initialized)
      {
# if HAVE_CFLOCALECOPYPREFERREDLANGUAGES /* MacOS X 10.5 or newer */
        CFArrayRef prefArray = CFLocaleCopyPreferredLanguages ();
# elif HAVE_CFPREFERENCESCOPYAPPVALUE /* MacOS X 10.4 or newer */
        CFTypeRef preferences =
          CFPreferencesCopyAppValue (CFSTR ("AppleLanguages"),
                                     kCFPreferencesCurrentApplication);
        if (preferences != NULL
            && CFGetTypeID (preferences) == CFArrayGetTypeID ())
          {
            CFArrayRef prefArray = (CFArrayRef)preferences;
# endif

            int n = CFArrayGetCount (prefArray);
            char buf[256];
            char buf2[256];
            size_t size = 0;
            int i;

            for (i = 0; i < n; i++)
              {
                CFTypeRef element = CFArrayGetValueAtIndex (prefArray, i);
                if (element != NULL
                    && CFGetTypeID (element) == CFStringGetTypeID ()
                    && CFStringGetCString ((CFStringRef)element,
                                           buf, sizeof (buf),
                                           kCFStringEncodingASCII))
                  {
                    strcpy (buf2, buf);
                    gl_locale_name_canonicalize (buf);
                    size += strlen (buf) + 1;
                    /* Mac OS X 10.12 or newer returns an array of elements of
                       the form "ll-CC" or "ll-Scrp-CC" where ll is a language
                       code, CC is a country code, and Scrp (optional) is a
                       script code.
                       gl_locale_name_canonicalize converts this to "ll_CC" or
                       "ll_Scrp_CC".
                       Sometimes ll and CC are unrelated, i.e. there is no
                       translation for "ll_CC" but one for "ll".
                       Similarly, in the case with a script, sometimes there is
                       no translation for "ll_Scrp_CC" but one for "ll_Scrp"
                       (after proper canonicalization).
                       Therefore, in the result, we return "ll_CC" followed
                       by "ll", or similarly for the case with a script.  */
                    {
                      char *last_minus = strrchr (buf2, '-');
                      if (last_minus != NULL)
                        {
                          *last_minus = '\0';
                          gl_locale_name_canonicalize (buf2);
                          size += strlen (buf2) + 1;
                        }
                    }
                    /* Most GNU programs use msgids in English and don't ship
                       an en.mo message catalog.  Therefore when we see "en" or
                       "en-CC" in the preferences list, arrange for gettext()
                       to return the msgid, and ignore all further elements of
                       the preferences list.  */
                    if (buf[0] == 'e' && buf[1] == 'n'
                        && (buf[2] == '\0' || buf[2] == '_'))
                      break;
                  }
                else
                  break;
              }
            if (size > 0)
              {
                char *languages = (char *) malloc (size);

                if (languages != NULL)
                  {
                    char *p = languages;

                    for (i = 0; i < n; i++)
                      {
                        CFTypeRef element =
                          CFArrayGetValueAtIndex (prefArray, i);
                        if (element != NULL
                            && CFGetTypeID (element) == CFStringGetTypeID ()
                            && CFStringGetCString ((CFStringRef)element,
                                                   buf, sizeof (buf),
                                                   kCFStringEncodingASCII))
                          {
                            strcpy (buf2, buf);
                            gl_locale_name_canonicalize (buf);
                            strcpy (p, buf);
                            p += strlen (buf);
                            *p++ = ':';
                            {
                              char *last_minus = strrchr (buf2, '-');
                              if (last_minus != NULL)
                                {
                                  *last_minus = '\0';
                                  gl_locale_name_canonicalize (buf2);
                                  strcpy (p, buf2);
                                  p += strlen (buf2);
                                  *p++ = ':';
                                }
                            }
                            if (buf[0] == 'e' && buf[1] == 'n'
                                 && (buf[2] == '\0' || buf[2] == '_'))
                              break;
                          }
                        else
                          break;
                      }
                    *--p = '\0';

                    cached_languages = languages;
                  }
              }

# if HAVE_CFLOCALECOPYPREFERREDLANGUAGES /* MacOS X 10.5 or newer */
        CFRelease (prefArray);
# elif HAVE_CFPREFERENCESCOPYAPPVALUE /* MacOS X 10.4 or newer */
          }
# endif
        cache_initialized = 1;
      }
    if (cached_languages != NULL)
      return cached_languages;
  }
#endif

#ifdef WIN32_NATIVE
  {
    /* Cache the preferences list, since computing it is expensive.  */
    static const char *cached_languages;
    static int cache_initialized;

    /* Activate the new code only when the GETTEXT_MUI environment variable is
       set, for the time being, since the new code is not well tested.  */
    if (!cache_initialized && getenv ("GETTEXT_MUI") != NULL)
      {
        const char *languages = NULL;
        HMODULE kernel32 = GetModuleHandle ("kernel32");

        if (kernel32 != NULL)
          languages = _nl_language_preferences_win32_mui (kernel32);

        if (languages == NULL && kernel32 != NULL)
          languages = _nl_language_preferences_win32_ME (kernel32);

        if (languages == NULL)
          languages = _nl_language_preferences_win32_95 ();

        if (languages == NULL && kernel32 != NULL)
          languages = _nl_language_preferences_win32_system (kernel32);

        cached_languages = languages;
        cache_initialized = 1;
      }
    if (cached_languages != NULL)
      return cached_languages;
  }
#endif

  return NULL;
}
