/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "LocaleService.h"

#include <algorithm>  // find_if()
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Omnijar.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/intl/OSPreferences.h"
#include "nsIObserverService.h"
#include "nsIToolkitChromeRegistry.h"
#include "nsStringEnumerator.h"
#include "nsXULAppAPI.h"
#include "nsZipArchive.h"

#include "unicode/uloc.h"

#define INTL_SYSTEM_LOCALES_CHANGED "intl:system-locales-changed"

#define MATCH_OS_LOCALE_PREF "intl.locale.matchOS"
#define SELECTED_LOCALE_PREF "general.useragent.locale"

static const char* kObservedPrefs[] = {
  MATCH_OS_LOCALE_PREF,
  SELECTED_LOCALE_PREF,
  nullptr
};

using namespace mozilla::intl;
using namespace mozilla;

NS_IMPL_ISUPPORTS(LocaleService, mozILocaleService, nsIObserver,
                  nsISupportsWeakReference)

mozilla::StaticRefPtr<LocaleService> LocaleService::sInstance;

/**
 * This function transforms a canonical Mozilla Language Tag, into it's
 * BCP47 compilant form.
 *
 * Example: "ja-JP-mac" -> "ja-JP-x-lvariant-mac"
 *
 * The BCP47 form should be used for all calls to ICU/Intl APIs.
 * The canonical form is used for all internal operations.
 */
static void
SanitizeForBCP47(nsACString& aLocale)
{
  // Currently, the only locale code we use that's not BCP47-conformant is
  // "ja-JP-mac" on OS X, but let's try to be more general than just
  // hard-coding that here.
  const int32_t LANG_TAG_CAPACITY = 128;
  char langTag[LANG_TAG_CAPACITY];
  nsAutoCString locale(aLocale);
  UErrorCode err = U_ZERO_ERROR;
  // This is a fail-safe method that will set langTag to "und" if it cannot
  // match any part of the input locale code.
  int32_t len = uloc_toLanguageTag(locale.get(), langTag, LANG_TAG_CAPACITY,
                                   false, &err);
  if (U_SUCCESS(err) && len > 0) {
    aLocale.Assign(langTag, len);
  }
}

static bool
ReadRequestedLocales(nsTArray<nsCString>& aRetVal)
{
  nsAutoCString locale;

  // First, we'll try to check if the user has `matchOS` pref selected
  bool matchOSLocale = Preferences::GetBool(MATCH_OS_LOCALE_PREF);

  if (matchOSLocale) {
    // If he has, we'll pick the locale from the system
    if (OSPreferences::GetInstance()->GetSystemLocales(aRetVal)) {
      // If we succeeded, return.
      return true;
    }
  }

  // Otherwise, we'll try to get the requested locale from the prefs.
  if (!NS_SUCCEEDED(Preferences::GetCString(SELECTED_LOCALE_PREF, locale))) {
    return false;
  }

  // At the moment we just take a single locale, but in the future
  // we'll want to allow user to specify a list of requested locales.
  aRetVal.AppendElement(locale);

  // en-US is a LastResort locale. LastResort locale is a fallback locale
  // for the requested locale chain. In the future we'll want to make the
  // fallback chain differ per-locale. For now, it'll always fallback on en-US.
  //
  // Notice: This is not the same as DefaultLocale,
  // which follows the default locale the build is in.
  if (!locale.Equals("en-US")) {
    aRetVal.AppendElement("en-US");
  }
  return true;
}

static bool
ReadAvailableLocales(nsTArray<nsCString>& aRetVal)
{
  nsCOMPtr<nsIToolkitChromeRegistry> cr =
    mozilla::services::GetToolkitChromeRegistryService();
  if (!cr) {
    return false;
  }

  nsCOMPtr<nsIUTF8StringEnumerator> localesEnum;

  nsresult rv =
    cr->GetLocalesForPackage(NS_LITERAL_CSTRING("global"), getter_AddRefs(localesEnum));
  if (!NS_SUCCEEDED(rv)) {
    return false;
  }

  bool more;
  while (NS_SUCCEEDED(rv = localesEnum->HasMore(&more)) && more) {
    nsAutoCString localeStr;
    rv = localesEnum->GetNext(localeStr);
    if (!NS_SUCCEEDED(rv)) {
      return false;
    }

    aRetVal.AppendElement(localeStr);
  }
  return !aRetVal.IsEmpty();
}

LocaleService::LocaleService(bool aIsServer)
  :mIsServer(aIsServer)
{
}

/**
 * This function performs the actual language negotiation for the API.
 *
 * Currently it collects the locale ID used by nsChromeRegistry and
 * adds hardcoded "en-US" locale as a fallback.
 */
void
LocaleService::NegotiateAppLocales(nsTArray<nsCString>& aRetVal)
{
  nsAutoCString defaultLocale;
  GetDefaultLocale(defaultLocale);

  if (mIsServer) {
    AutoTArray<nsCString, 100> availableLocales;
    AutoTArray<nsCString, 10> requestedLocales;
    GetAvailableLocales(availableLocales);
    GetRequestedLocales(requestedLocales);

    NegotiateLanguages(requestedLocales, availableLocales, defaultLocale,
                       LangNegStrategy::Filtering, aRetVal);
  } else {
    // In content process, we will not do any language negotiation.
    // Instead, the language is set manually by SetAppLocales.
    //
    // If this method has been called, it means that we did not fire
    // SetAppLocales yet (happens during initialization).
    // In that case, all we can do is return the default locale.
    // Once SetAppLocales will be called later, it'll fire an event
    // allowing callers to update the locale.
    aRetVal.AppendElement(defaultLocale);
  }
}

LocaleService*
LocaleService::GetInstance()
{
  if (!sInstance) {
    sInstance = new LocaleService(XRE_IsParentProcess());

    if (sInstance->IsServer()) {
      // We're going to observe for requested languages changes which come
      // from prefs.
      DebugOnly<nsresult> rv = Preferences::AddWeakObservers(sInstance, kObservedPrefs);
      MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");

      nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
      if (obs) {
        obs->AddObserver(sInstance, INTL_SYSTEM_LOCALES_CHANGED, true);
      }
    }
    ClearOnShutdown(&sInstance, ShutdownPhase::Shutdown);
  }
  return sInstance;
}

LocaleService::~LocaleService()
{
  if (mIsServer) {
    Preferences::RemoveObservers(this, kObservedPrefs);

    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    if (obs) {
      obs->RemoveObserver(this, INTL_SYSTEM_LOCALES_CHANGED);
    }
  }
}

void
LocaleService::GetAppLocalesAsLangTags(nsTArray<nsCString>& aRetVal)
{
  if (mAppLocales.IsEmpty()) {
    NegotiateAppLocales(mAppLocales);
  }
  aRetVal = mAppLocales;
}

void
LocaleService::GetAppLocalesAsBCP47(nsTArray<nsCString>& aRetVal)
{
  if (mAppLocales.IsEmpty()) {
    NegotiateAppLocales(mAppLocales);
  }
  for (uint32_t i = 0; i < mAppLocales.Length(); i++) {
    nsAutoCString locale(mAppLocales[i]);
    SanitizeForBCP47(locale);
    aRetVal.AppendElement(locale);
  }
}

void
LocaleService::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal)
{
  bool useOSLocales = Preferences::GetBool("intl.regional_prefs.use_os_locales", false);

  // If the user specified that they want to use OS Regional Preferences locales,
  // try to retrieve them and use.
  if (useOSLocales) {
    if (OSPreferences::GetInstance()->GetRegionalPrefsLocales(aRetVal)) {
      return;
    }

    // If we fail to retrieve them, return the app locales.
    GetAppLocalesAsBCP47(aRetVal);
    return;
  }

  // Otherwise, fetch OS Regional Preferences locales and compare the first one
  // to the app locale. If the language subtag matches, we can safely use
  // the OS Regional Preferences locale.
  //
  // This facilitates scenarios such as Firefox in "en-US" and User sets
  // regional prefs to "en-GB".
  nsAutoCString appLocale;
  AutoTArray<nsCString, 10> regionalPrefsLocales;
  LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocale);

  if (!OSPreferences::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales)) {
    GetAppLocalesAsBCP47(aRetVal);
    return;
  }

  if (LocaleService::LanguagesMatch(appLocale, regionalPrefsLocales[0])) {
    aRetVal = regionalPrefsLocales;
    return;
  }

  // Otherwise use the app locales.
  GetAppLocalesAsBCP47(aRetVal);
}

void
LocaleService::AssignAppLocales(const nsTArray<nsCString>& aAppLocales)
{
  MOZ_ASSERT(!mIsServer, "This should only be called for LocaleService in client mode.");

  mAppLocales = aAppLocales;
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr);
  }
}

void
LocaleService::AssignRequestedLocales(const nsTArray<nsCString>& aRequestedLocales)
{
  MOZ_ASSERT(!mIsServer, "This should only be called for LocaleService in client mode.");

  mRequestedLocales = aRequestedLocales;
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
  }
}

bool
LocaleService::GetRequestedLocales(nsTArray<nsCString>& aRetVal)
{
  if (mRequestedLocales.IsEmpty()) {
    ReadRequestedLocales(mRequestedLocales);

  }

  aRetVal = mRequestedLocales;
  return true;
}

bool
LocaleService::GetAvailableLocales(nsTArray<nsCString>& aRetVal)
{
  if (mAvailableLocales.IsEmpty()) {
    ReadAvailableLocales(mAvailableLocales);
  }

  aRetVal = mAvailableLocales;
  return true;
}


void
LocaleService::AvailableLocalesChanged()
{
  MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
  mAvailableLocales.Clear();
  // In the future we may want to trigger here intl:available-locales-changed
  LocalesChanged();
}

void
LocaleService::RequestedLocalesChanged()
{
  MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");

  nsTArray<nsCString> newLocales;
  ReadRequestedLocales(newLocales);

  if (mRequestedLocales != newLocales) {
    mRequestedLocales = Move(newLocales);
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    if (obs) {
      obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
    }
    LocalesChanged();
  }
}

void
LocaleService::LocalesChanged()
{
  MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");

  // if mAppLocales has not been initialized yet, just return
  if (mAppLocales.IsEmpty()) {
    return;
  }

  nsTArray<nsCString> newLocales;
  NegotiateAppLocales(newLocales);

  if (mAppLocales != newLocales) {
    mAppLocales = Move(newLocales);
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    if (obs) {
      obs->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr);

      // Deprecated, please use `intl:app-locales-changed`.
      // Kept for now for compatibility reasons
      obs->NotifyObservers(nullptr, "selected-locale-has-changed", nullptr);
    }
  }
}

// After trying each step of the negotiation algorithm for each requested locale,
// if a match was found we use this macro to decide whether to return immediately,
// skip to the next requested locale, or continue searching for additional matches,
// according to the desired negotiation strategy.
#define HANDLE_STRATEGY \
          switch (aStrategy) { \
            case LangNegStrategy::Lookup: \
              return; \
            case LangNegStrategy::Matching: \
              continue; \
            case LangNegStrategy::Filtering: \
              break; \
          }

/**
 * This is the raw algorithm for language negotiation based roughly
 * on RFC4647 language filtering, with changes from LDML language matching.
 *
 * The exact algorithm is custom, and consists of a 6 level strategy:
 *
 * 1) Attempt to find an exact match for each requested locale in available
 *    locales.
 *    Example: ['en-US'] * ['en-US'] = ['en-US']
 *
 * 2) Attempt to match a requested locale to an available locale treated
 *    as a locale range.
 *    Example: ['en-US'] * ['en'] = ['en']
 *                           ^^
 *                           |-- becomes 'en-*-*-*'
 *
 * 3) Attempt to use the maximized version of the requested locale, to
 *    find the best match in available locales.
 *    Example: ['en'] * ['en-GB', 'en-US'] = ['en-US']
 *               ^^
 *               |-- ICU likelySubtags expands it to 'en-Latn-US'
 *
 * 4) Attempt to look up for a different variant of the same locale.
 *    Example: ['ja-JP-win'] * ['ja-JP-mac'] = ['ja-JP-mac']
 *               ^^^^^^^^^
 *               |----------- replace variant with range: 'ja-JP-*'
 *
 * 5) Attempt to look up for a maximized version of the requested locale,
 *    stripped of the region code.
 *    Example: ['en-CA'] * ['en-ZA', 'en-US'] = ['en-US', 'en-ZA']
 *               ^^^^^
 *               |----------- look for likelySubtag of 'en': 'en-Latn-US'
 *
 *
 * 6) Attempt to look up for a different region of the same locale.
 *    Example: ['en-GB'] * ['en-AU'] = ['en-AU']
 *               ^^^^^
 *               |----- replace region with range: 'en-*'
 *
 * It uses one of the strategies described in LocaleService.h.
 */
void
LocaleService::FilterMatches(const nsTArray<nsCString>& aRequested,
                             const nsTArray<nsCString>& aAvailable,
                             LangNegStrategy aStrategy,
                             nsTArray<nsCString>& aRetVal)
{
  // Local copy of the list of available locales, in Locale form for flexible
  // matching. We will remove entries from this list as they get appended to
  // aRetVal, so that no available locale will be found more than once.
  AutoTArray<Locale, 100> availLocales;
  for (auto& avail : aAvailable) {
    availLocales.AppendElement(Locale(avail, true));
  }

  // Helper to erase an entry from availLocales once we have copied it to
  // the result list. Returns an iterator pointing to the entry that was
  // immediately after the one that was erased (or availLocales.end() if
  // the target was the last in the array).
  auto eraseFromAvail = [&](nsTArray<Locale>::iterator aIter) {
    nsTArray<Locale>::size_type index = aIter - availLocales.begin();
    availLocales.RemoveElementAt(index);
    return availLocales.begin() + index;
  };

  for (auto& requested : aRequested) {

    // 1) Try to find a simple (case-insensitive) string match for the request.
    auto matchesExactly = [&](const Locale& aLoc) {
      return requested.Equals(aLoc.AsString(),
                              nsCaseInsensitiveCStringComparator());
    };
    auto match = std::find_if(availLocales.begin(), availLocales.end(),
                              matchesExactly);
    if (match != availLocales.end()) {
      aRetVal.AppendElement(match->AsString());
      eraseFromAvail(match);
    }

    if (!aRetVal.IsEmpty()) {
      HANDLE_STRATEGY;
    }

    // 2) Try to match against the available locales treated as ranges.
    auto findRangeMatches = [&](const Locale& aReq) {
      auto matchesRange = [&](const Locale& aLoc) {
        return aReq.Matches(aLoc);
      };
      bool foundMatch = false;
      auto match = availLocales.begin();
      while ((match = std::find_if(match, availLocales.end(),
                                   matchesRange)) != availLocales.end()) {
        aRetVal.AppendElement(match->AsString());
        match = eraseFromAvail(match);
        foundMatch = true;
        if (aStrategy != LangNegStrategy::Filtering) {
          return true; // we only want the first match
        }
      }
      return foundMatch;
    };

    Locale requestedLocale = Locale(requested, false);
    if (findRangeMatches(requestedLocale)) {
      HANDLE_STRATEGY;
    }

    // 3) Try to match against a maximized version of the requested locale
    if (requestedLocale.AddLikelySubtags()) {
      if (findRangeMatches(requestedLocale)) {
        HANDLE_STRATEGY;
      }
    }

    // 4) Try to match against a variant as a range
    requestedLocale.SetVariantRange();
    if (findRangeMatches(requestedLocale)) {
      HANDLE_STRATEGY;
    }

    // 5) Try to match against the likely subtag without region
    if (requestedLocale.AddLikelySubtagsWithoutRegion()) {
      if (findRangeMatches(requestedLocale)) {
        HANDLE_STRATEGY;
      }
    }


    // 6) Try to match against a region as a range
    requestedLocale.SetRegionRange();
    if (findRangeMatches(requestedLocale)) {
      HANDLE_STRATEGY;
    }
  }
}

bool
LocaleService::NegotiateLanguages(const nsTArray<nsCString>& aRequested,
                                  const nsTArray<nsCString>& aAvailable,
                                  const nsACString& aDefaultLocale,
                                  LangNegStrategy aStrategy,
                                  nsTArray<nsCString>& aRetVal)
{
  // If the strategy is Lookup, we require the defaultLocale to be set.
  if (aStrategy == LangNegStrategy::Lookup && aDefaultLocale.IsEmpty()) {
    return false;
  }

  FilterMatches(aRequested, aAvailable, aStrategy, aRetVal);

  if (aStrategy == LangNegStrategy::Lookup) {
    if (aRetVal.Length() == 0) {
      // If the strategy is Lookup and Filtering returned no matches, use
      // the default locale.
      aRetVal.AppendElement(aDefaultLocale);
    }
  } else if (!aDefaultLocale.IsEmpty() && !aRetVal.Contains(aDefaultLocale)) {
    // If it's not a Lookup strategy, add the default locale only if it's
    // set and it's not in the results already.
    aRetVal.AppendElement(aDefaultLocale);
  }
  return true;
}

bool
LocaleService::IsAppLocaleRTL()
{
  nsAutoCString locale;
  GetAppLocaleAsBCP47(locale);

  int pref = Preferences::GetInt("intl.uidirection", -1);
  if (pref >= 0) {
    return (pref > 0);
  }
  return uloc_isRightToLeft(locale.get());
}

NS_IMETHODIMP
LocaleService::Observe(nsISupports *aSubject, const char *aTopic,
                      const char16_t *aData)
{
  MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");

  if (!strcmp(aTopic, INTL_SYSTEM_LOCALES_CHANGED)) {
    RequestedLocalesChanged();
  } else {
    NS_ConvertUTF16toUTF8 pref(aData);
    // At the moment the only thing we're observing are settings indicating
    // user requested locales.
    if (pref.EqualsLiteral(MATCH_OS_LOCALE_PREF) ||
        pref.EqualsLiteral(SELECTED_LOCALE_PREF)) {
      RequestedLocalesChanged();
    }
  }

  return NS_OK;
}

bool
LocaleService::LanguagesMatch(const nsCString& aRequested,
                              const nsCString& aAvailable)
{
  return Locale(aRequested, true).LanguageMatches(Locale(aAvailable, true));
}


bool
LocaleService::IsServer()
{
  return mIsServer;
}

/**
 * mozILocaleService methods
 */

static char**
CreateOutArray(const nsTArray<nsCString>& aArray)
{
  uint32_t n = aArray.Length();
  char** result = static_cast<char**>(moz_xmalloc(n * sizeof(char*)));
  for (uint32_t i = 0; i < n; i++) {
    result[i] = moz_xstrdup(aArray[i].get());
  }
  return result;
}

NS_IMETHODIMP
LocaleService::GetDefaultLocale(nsACString& aRetVal)
{
  // We don't allow this to change during a session (it's set at build/package
  // time), so we cache the result the first time we're called.
  if (mDefaultLocale.IsEmpty()) {
    // Try to get the package locale from update.locale in omnijar. If the
    // update.locale file is not found, item.len will remain 0 and we'll
    // just use our hard-coded default below.
    // (We could also search for an update.locale file in the GRE resources
    // directory, to support non-packaged builds, but that seems like a lot
    // of extra code for what is probably not an important use case.)
    RefPtr<nsZipArchive> zip = Omnijar::GetReader(Omnijar::GRE);
    if (zip) {
      nsZipItemPtr<char> item(zip, "update.locale");
      size_t len = item.Length();
      // Ignore any trailing spaces, newlines, etc.
      while (len > 0 && item.Buffer()[len - 1] <= ' ') {
        len--;
      }
      mDefaultLocale.Assign(item.Buffer(), len);
    }
    // Hard-coded fallback, e.g. for non-packaged developer builds.
    // XXX Is there any reason to make this a compile-time #define that
    // can be set via configure or something?
    if (mDefaultLocale.IsEmpty()) {
      mDefaultLocale.AssignLiteral("en-US");
    }
  }

  aRetVal = mDefaultLocale;
  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetAppLocalesAsLangTags(uint32_t* aCount, char*** aOutArray)
{
  if (mAppLocales.IsEmpty()) {
    NegotiateAppLocales(mAppLocales);
  }

  *aCount = mAppLocales.Length();
  *aOutArray = CreateOutArray(mAppLocales);

  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetAppLocalesAsBCP47(uint32_t* aCount, char*** aOutArray)
{
  AutoTArray<nsCString, 32> locales;
  GetAppLocalesAsBCP47(locales);

  *aCount = locales.Length();
  *aOutArray = CreateOutArray(locales);

  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetAppLocaleAsLangTag(nsACString& aRetVal)
{
  if (mAppLocales.IsEmpty()) {
    NegotiateAppLocales(mAppLocales);
  }
  aRetVal = mAppLocales[0];
  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetAppLocaleAsBCP47(nsACString& aRetVal)
{
  if (mAppLocales.IsEmpty()) {
    NegotiateAppLocales(mAppLocales);
  }
  aRetVal = mAppLocales[0];

  SanitizeForBCP47(aRetVal);
  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetRegionalPrefsLocales(uint32_t* aCount, char*** aOutArray)
{
  AutoTArray<nsCString,10> rgLocales;

  GetRegionalPrefsLocales(rgLocales);

  *aCount = rgLocales.Length();
  *aOutArray = static_cast<char**>(moz_xmalloc(*aCount * sizeof(char*)));

  for (uint32_t i = 0; i < *aCount; i++) {
    (*aOutArray)[i] = moz_xstrdup(rgLocales[i].get());
  }

  return NS_OK;
}

static LocaleService::LangNegStrategy
ToLangNegStrategy(int32_t aStrategy)
{
  switch (aStrategy) {
    case 1:
      return LocaleService::LangNegStrategy::Matching;
    case 2:
      return LocaleService::LangNegStrategy::Lookup;
    default:
      return LocaleService::LangNegStrategy::Filtering;
  }
}

NS_IMETHODIMP
LocaleService::NegotiateLanguages(const char** aRequested,
                                  const char** aAvailable,
                                  const char*  aDefaultLocale,
                                  int32_t aStrategy,
                                  uint32_t aRequestedCount,
                                  uint32_t aAvailableCount,
                                  uint32_t* aCount, char*** aRetVal)
{
  if (aStrategy < 0 || aStrategy > 2) {
    return NS_ERROR_INVALID_ARG;
  }

  // Check that the given string contains only ASCII characters valid in tags
  // (i.e. alphanumerics, plus '-' and '_'), and is non-empty.
  auto validTagChars = [](const char* s) {
    if (!s || !*s) {
      return false;
    }
    while (*s) {
      if (isalnum((unsigned char)*s) || *s == '-' || *s == '_' || *s == '*') {
        s++;
      } else {
        return false;
      }
    }
    return true;
  };

  AutoTArray<nsCString, 100> requestedLocales;
  for (uint32_t i = 0; i < aRequestedCount; i++) {
    if (!validTagChars(aRequested[i])) {
      continue;
    }
    requestedLocales.AppendElement(aRequested[i]);
  }

  AutoTArray<nsCString, 100> availableLocales;
  for (uint32_t i = 0; i < aAvailableCount; i++) {
    if (!validTagChars(aAvailable[i])) {
      continue;
    }
    availableLocales.AppendElement(aAvailable[i]);
  }

  nsAutoCString defaultLocale(aDefaultLocale);

  LangNegStrategy strategy = ToLangNegStrategy(aStrategy);

  AutoTArray<nsCString, 100> supportedLocales;
  bool result = NegotiateLanguages(requestedLocales, availableLocales,
                                   defaultLocale, strategy, supportedLocales);

  if (!result) {
    return NS_ERROR_INVALID_ARG;
  }

  *aRetVal =
    static_cast<char**>(moz_xmalloc(sizeof(char*) * supportedLocales.Length()));

  *aCount = 0;
  for (const auto& supported : supportedLocales) {
    (*aRetVal)[(*aCount)++] = moz_xstrdup(supported.get());
  }

  return NS_OK;
}

LocaleService::Locale::Locale(const nsCString& aLocale, bool aRange)
  : mLocaleStr(aLocale)
{
  int32_t partNum = 0;

  nsAutoCString normLocale(aLocale);
  normLocale.ReplaceChar('_', '-');

  for (const nsACString& part : normLocale.Split('-')) {
    switch (partNum) {
      case 0:
        if (part.EqualsLiteral("*") ||
            part.Length() == 2 || part.Length() == 3) {
          mLanguage.Assign(part);
        }
        break;
      case 1:
        if (part.EqualsLiteral("*") || part.Length() == 4) {
          mScript.Assign(part);
          break;
        }

        // fallover to region case
        partNum++;
        MOZ_FALLTHROUGH;
      case 2:
        if (part.EqualsLiteral("*") || part.Length() == 2) {
          mRegion.Assign(part);
        }
        break;
      case 3:
        if (part.EqualsLiteral("*") || part.Length() == 3) {
          mVariant.Assign(part);
        }
        break;
    }
    partNum++;
  }

  if (aRange) {
    if (mLanguage.IsEmpty()) {
      mLanguage.AssignLiteral("*");
    }
    if (mScript.IsEmpty()) {
      mScript.AssignLiteral("*");
    }
    if (mRegion.IsEmpty()) {
      mRegion.AssignLiteral("*");
    }
    if (mVariant.IsEmpty()) {
      mVariant.AssignLiteral("*");
    }
  }
}

static bool
SubtagMatches(const nsCString& aSubtag1, const nsCString& aSubtag2)
{
  return aSubtag1.EqualsLiteral("*") ||
         aSubtag2.EqualsLiteral("*") ||
         aSubtag1.Equals(aSubtag2, nsCaseInsensitiveCStringComparator());
}

bool
LocaleService::Locale::Matches(const LocaleService::Locale& aLocale) const
{
  return SubtagMatches(mLanguage, aLocale.mLanguage) &&
         SubtagMatches(mScript, aLocale.mScript) &&
         SubtagMatches(mRegion, aLocale.mRegion) &&
         SubtagMatches(mVariant, aLocale.mVariant);
}

bool
LocaleService::Locale::LanguageMatches(const LocaleService::Locale& aLocale) const
{
  return SubtagMatches(mLanguage, aLocale.mLanguage) &&
         SubtagMatches(mScript, aLocale.mScript);
}

void
LocaleService::Locale::SetVariantRange()
{
  mVariant.AssignLiteral("*");
}

void
LocaleService::Locale::SetRegionRange()
{
  mRegion.AssignLiteral("*");
}

bool
LocaleService::Locale::AddLikelySubtags()
{
  return AddLikelySubtagsForLocale(mLocaleStr);
}

bool
LocaleService::Locale::AddLikelySubtagsWithoutRegion()
{
  nsAutoCString locale(mLanguage);

  if (!mScript.IsEmpty()) {
    locale.Append("-");
    locale.Append(mScript);
  }

  // We don't add variant here because likelySubtag doesn't care about it.

  return AddLikelySubtagsForLocale(locale);
}

bool
LocaleService::Locale::AddLikelySubtagsForLocale(const nsACString& aLocale)
{
  const int32_t kLocaleMax = 160;
  char maxLocale[kLocaleMax];
  nsAutoCString locale(aLocale);

  UErrorCode status = U_ZERO_ERROR;
  uloc_addLikelySubtags(locale.get(), maxLocale, kLocaleMax, &status);

  if (U_FAILURE(status)) {
    return false;
  }

  nsDependentCString maxLocStr(maxLocale);
  Locale loc = Locale(maxLocStr, false);

  if (loc == *this) {
    return false;
  }

  mLanguage = loc.mLanguage;
  mScript = loc.mScript;
  mRegion = loc.mRegion;

  // We don't update variant from likelySubtag since it's not going to
  // provide it and we want to preserve the range

  return true;
}

NS_IMETHODIMP
LocaleService::GetRequestedLocales(uint32_t* aCount, char*** aOutArray)
{
  AutoTArray<nsCString, 16> requestedLocales;
  bool res = GetRequestedLocales(requestedLocales);

  if (!res) {
    NS_ERROR("Couldn't retrieve selected locales from prefs!");
    return NS_ERROR_FAILURE;
  }

  *aCount = requestedLocales.Length();
  *aOutArray = CreateOutArray(requestedLocales);

  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetRequestedLocale(nsACString& aRetVal)
{
  AutoTArray<nsCString, 16> requestedLocales;
  bool res = GetRequestedLocales(requestedLocales);

  if (!res) {
    NS_ERROR("Couldn't retrieve selected locales from prefs!");
    return NS_ERROR_FAILURE;
  }

  if (requestedLocales.Length() > 0) {
    aRetVal = requestedLocales[0];
  }

  return NS_OK;
}

NS_IMETHODIMP
LocaleService::SetRequestedLocales(const char** aRequested,
                                   uint32_t aRequestedCount)
{
  MOZ_ASSERT(aRequestedCount < 2 ||
             (aRequestedCount == 2 && strcmp(aRequested[1], "en-US") == 0),
      "We can only handle one requested locale (optionally with en-US last fallback)");

  if (aRequestedCount == 0) {
    Preferences::ClearUser(SELECTED_LOCALE_PREF);
  } else {
    Preferences::SetCString(SELECTED_LOCALE_PREF, aRequested[0]);
  }

  Preferences::SetBool(MATCH_OS_LOCALE_PREF, aRequestedCount == 0);
  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetAvailableLocales(uint32_t* aCount, char*** aOutArray)
{
  AutoTArray<nsCString, 100> availableLocales;
  bool res = GetAvailableLocales(availableLocales);

  if (!res) {
    NS_ERROR("Couldn't retrieve available locales!");
    return NS_ERROR_FAILURE;
  }

  *aCount = availableLocales.Length();
  *aOutArray = CreateOutArray(availableLocales);
  return NS_OK;
}

NS_IMETHODIMP
LocaleService::GetIsAppLocaleRTL(bool* aRetVal)
{
  (*aRetVal) = IsAppLocaleRTL();
  return NS_OK;
}
