/* 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/. */

/**
 * This is a shared part of the OSPreferences API implementation.
 * It defines helper methods and public methods that are calling
 * platform-specific private methods.
 */

#include "OSPreferences.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/intl/DateTimePatternGenerator.h"
#include "mozilla/intl/DateTimeFormat.h"
#include "mozilla/intl/LocaleService.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "nsIObserverService.h"

using namespace mozilla::intl;

NS_IMPL_ISUPPORTS(OSPreferences, mozIOSPreferences)

mozilla::StaticRefPtr<OSPreferences> OSPreferences::sInstance;

// Return a new strong reference to the instance, creating it if necessary.
already_AddRefed<OSPreferences> OSPreferences::GetInstanceAddRefed() {
  RefPtr<OSPreferences> result = sInstance;
  if (!result) {
    MOZ_ASSERT(NS_IsMainThread(),
               "OSPreferences should be initialized on main thread!");
    if (!NS_IsMainThread()) {
      return nullptr;
    }
    sInstance = new OSPreferences();
    result = sInstance;

    DebugOnly<nsresult> rv = Preferences::RegisterPrefixCallback(
        PreferenceChanged, "intl.date_time.pattern_override");
    MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");

    ClearOnShutdown(&sInstance);
  }
  return result.forget();
}

// Return a raw pointer to the instance: not for off-main-thread use,
// because ClearOnShutdown means it could go away unexpectedly.
OSPreferences* OSPreferences::GetInstance() {
  MOZ_ASSERT(NS_IsMainThread());
  if (!sInstance) {
    // This will create the static instance; then we just drop the extra
    // reference.
    RefPtr<OSPreferences> result = GetInstanceAddRefed();
  }
  return sInstance;
}

void OSPreferences::Refresh() {
  // TODO: in content processes, get system locales from the parent process
  // to ensure consistency and avoid depending on APIs that may be blocked
  // by sandboxing (see bug 1946691).
  // (Not strictly necessary for now as we don't currently use Refresh() on
  // Windows.)

  nsTArray<nsCString> newLocales;
  ReadSystemLocales(newLocales);

  if (mSystemLocales != newLocales) {
    mSystemLocales = std::move(newLocales);
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    if (obs) {
      obs->NotifyObservers(nullptr, "intl:system-locales-changed", nullptr);
    }
  }
}

OSPreferences::~OSPreferences() {
  Preferences::UnregisterPrefixCallback(PreferenceChanged,
                                        "intl.date_time.pattern_override");
  RemoveObservers();
}

/*static*/
void OSPreferences::PreferenceChanged(const char* aPrefName,
                                      void* /* aClosure */) {
  if (sInstance) {
    sInstance->mPatternCache.Clear();
  }
}

/**
 * This method should be called by every method of OSPreferences that
 * retrieves a locale id from external source.
 *
 * It attempts to retrieve as much of the locale ID as possible, cutting
 * out bits that are not understood (non-strict behavior of ICU).
 *
 * It returns true if the canonicalization was successful.
 */
bool OSPreferences::CanonicalizeLanguageTag(nsCString& aLoc) {
  return LocaleService::CanonicalizeLanguageId(aLoc);
}

/**
 * This method retrieves from mozilla::intl the best pattern for a given
 * date/time style.
 */
bool OSPreferences::GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,
                                               DateTimeFormatStyle aTimeStyle,
                                               const nsACString& aLocale,
                                               nsACString& aRetVal) {
  DateTimeFormat::StyleBag style;

  switch (aTimeStyle) {
    case DateTimeFormatStyle::Short:
      style.time = Some(DateTimeFormat::Style::Short);
      break;
    case DateTimeFormatStyle::Medium:
      style.time = Some(DateTimeFormat::Style::Medium);
      break;
    case DateTimeFormatStyle::Long:
      style.time = Some(DateTimeFormat::Style::Long);
      break;
    case DateTimeFormatStyle::Full:
      style.time = Some(DateTimeFormat::Style::Full);
      break;
    case DateTimeFormatStyle::None:
    case DateTimeFormatStyle::Invalid:
      // Do nothing.
      break;
  }

  switch (aDateStyle) {
    case DateTimeFormatStyle::Short:
      style.date = Some(DateTimeFormat::Style::Short);
      break;
    case DateTimeFormatStyle::Medium:
      style.date = Some(DateTimeFormat::Style::Medium);
      break;
    case DateTimeFormatStyle::Long:
      style.date = Some(DateTimeFormat::Style::Long);
      break;
    case DateTimeFormatStyle::Full:
      style.date = Some(DateTimeFormat::Style::Full);
      break;
    case DateTimeFormatStyle::None:
    case DateTimeFormatStyle::Invalid:
      // Do nothing.
      break;
  }

  nsAutoCString locale;
  if (aLocale.IsEmpty()) {
    AutoTArray<nsCString, 10> regionalPrefsLocales;
    LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
    locale.Assign(regionalPrefsLocales[0]);
  } else {
    locale.Assign(aLocale);
  }

  auto genResult =
      DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
  if (genResult.isErr()) {
    return false;
  }
  auto generator = genResult.unwrap();

  auto dfResult = DateTimeFormat::TryCreateFromStyle(
      MakeStringSpan(locale.get()), style, generator.get(), Nothing());
  if (dfResult.isErr()) {
    return false;
  }
  auto df = dfResult.unwrap();

  DateTimeFormat::PatternVector pattern;
  auto patternResult = df->GetPattern(pattern);
  if (patternResult.isErr()) {
    return false;
  }

  aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
  return true;
}

/**
 * This method retrieves from mozilla::intl the best skeleton for a given
 * date/time style.
 *
 * This is useful for cases where an OS does not provide its own patterns,
 * but provide ability to customize the skeleton, like alter hourCycle setting.
 *
 * The returned value is a skeleton that matches the styles.
 */
bool OSPreferences::GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,
                                                DateTimeFormatStyle aTimeStyle,
                                                const nsACString& aLocale,
                                                nsACString& aRetVal) {
  nsAutoCString pattern;
  if (!GetDateTimePatternForStyle(aDateStyle, aTimeStyle, aLocale, pattern)) {
    return false;
  }

  auto genResult =
      DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
  if (genResult.isErr()) {
    return false;
  }

  nsAutoString patternAsUtf16 = NS_ConvertUTF8toUTF16(pattern);
  DateTimeFormat::SkeletonVector skeleton;
  auto generator = genResult.unwrap();
  auto skeletonResult = generator->GetSkeleton(patternAsUtf16, skeleton);
  if (skeletonResult.isErr()) {
    return false;
  }

  aRetVal = NS_ConvertUTF16toUTF8(skeleton.begin(), skeleton.length());
  return true;
}

/**
 * This method checks for preferences that override the defaults
 */
bool OSPreferences::OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,
                                            DateTimeFormatStyle aTimeStyle,
                                            const nsACString& aLocale,
                                            nsACString& aRetVal) {
  const auto PrefToMaybeString = [](const char* pref) -> Maybe<nsAutoCString> {
    nsAutoCString value;
    nsresult nr = Preferences::GetCString(pref, value);
    if (NS_FAILED(nr) || value.IsEmpty()) {
      return Nothing();
    }
    return Some(std::move(value));
  };

  Maybe<nsAutoCString> timeSkeleton;
  switch (aTimeStyle) {
    case DateTimeFormatStyle::Short:
      timeSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.time_short");
      break;
    case DateTimeFormatStyle::Medium:
      timeSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.time_medium");
      break;
    case DateTimeFormatStyle::Long:
      timeSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.time_long");
      break;
    case DateTimeFormatStyle::Full:
      timeSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.time_full");
      break;
    default:
      break;
  }

  Maybe<nsAutoCString> dateSkeleton;
  switch (aDateStyle) {
    case DateTimeFormatStyle::Short:
      dateSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.date_short");
      break;
    case DateTimeFormatStyle::Medium:
      dateSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.date_medium");
      break;
    case DateTimeFormatStyle::Long:
      dateSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.date_long");
      break;
    case DateTimeFormatStyle::Full:
      dateSkeleton =
          PrefToMaybeString("intl.date_time.pattern_override.date_full");
      break;
    default:
      break;
  }

  nsAutoCString locale;
  if (aLocale.IsEmpty()) {
    AutoTArray<nsCString, 10> regionalPrefsLocales;
    LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
    locale.Assign(regionalPrefsLocales[0]);
  } else {
    locale.Assign(aLocale);
  }

  const auto FillConnectorPattern = [&locale](
                                        const nsAutoCString& datePattern,
                                        const nsAutoCString& timePattern) {
    nsAutoCString pattern;
    GetDateTimeConnectorPattern(nsDependentCString(locale.get()), pattern);
    int32_t index = pattern.Find("{1}");
    if (index != kNotFound) {
      pattern.Replace(index, 3, datePattern);
    }
    index = pattern.Find("{0}");
    if (index != kNotFound) {
      pattern.Replace(index, 3, timePattern);
    }
    return pattern;
  };

  if (timeSkeleton && dateSkeleton) {
    aRetVal.Assign(FillConnectorPattern(*dateSkeleton, *timeSkeleton));
  } else if (timeSkeleton) {
    if (aDateStyle != DateTimeFormatStyle::None) {
      nsAutoCString pattern;
      if (!ReadDateTimePattern(aDateStyle, DateTimeFormatStyle::None, aLocale,
                               pattern) &&
          !GetDateTimePatternForStyle(aDateStyle, DateTimeFormatStyle::None,
                                      aLocale, pattern)) {
        return false;
      }
      aRetVal.Assign(FillConnectorPattern(pattern, *timeSkeleton));
    } else {
      aRetVal.Assign(*timeSkeleton);
    }
  } else if (dateSkeleton) {
    if (aTimeStyle != DateTimeFormatStyle::None) {
      nsAutoCString pattern;
      if (!ReadDateTimePattern(DateTimeFormatStyle::None, aTimeStyle, aLocale,
                               pattern) &&
          !GetDateTimePatternForStyle(DateTimeFormatStyle::None, aTimeStyle,
                                      aLocale, pattern)) {
        return false;
      }
      aRetVal.Assign(FillConnectorPattern(*dateSkeleton, pattern));
    } else {
      aRetVal.Assign(*dateSkeleton);
    }
  } else {
    return false;
  }

  return true;
}

/**
 * This function is a counterpart to GetDateTimeSkeletonForStyle.
 *
 * It takes a skeleton and returns the best available pattern for a given locale
 * that represents the provided skeleton.
 *
 * For example:
 * "Hm" skeleton for "en-US" will return "H:m"
 */
bool OSPreferences::GetPatternForSkeleton(const nsACString& aSkeleton,
                                          const nsACString& aLocale,
                                          nsACString& aRetVal) {
  aRetVal.Truncate();

  auto genResult =
      DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
  if (genResult.isErr()) {
    return false;
  }

  nsAutoString skeletonAsUtf16 = NS_ConvertUTF8toUTF16(aSkeleton);
  DateTimeFormat::PatternVector pattern;
  auto generator = genResult.unwrap();
  auto patternResult = generator->GetBestPattern(skeletonAsUtf16, pattern);
  if (patternResult.isErr()) {
    return false;
  }

  aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
  return true;
}

/**
 * This function returns a pattern that should be used to join date and time
 * patterns into a single date/time pattern string.
 *
 * It's useful for OSes that do not provide an API to retrieve such combined
 * pattern.
 *
 * An example output is "{1}, {0}".
 */
bool OSPreferences::GetDateTimeConnectorPattern(const nsACString& aLocale,
                                                nsACString& aRetVal) {
  // Check for a valid override pref and use that if present.
  nsAutoCString value;
  nsresult nr = Preferences::GetCString(
      "intl.date_time.pattern_override.connector_short", value);
  if (NS_SUCCEEDED(nr) && value.Find("{0}") != kNotFound &&
      value.Find("{1}") != kNotFound) {
    aRetVal = std::move(value);
    return true;
  }

  auto genResult =
      DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
  if (genResult.isErr()) {
    return false;
  }

  auto generator = genResult.unwrap();
  Span<const char16_t> result = generator->GetPlaceholderPattern();
  aRetVal = NS_ConvertUTF16toUTF8(result.data(), result.size());
  return true;
}

/**
 * mozIOSPreferences methods
 */
NS_IMETHODIMP
OSPreferences::GetSystemLocales(nsTArray<nsCString>& aRetVal) {
  if (!mSystemLocales.IsEmpty()) {
    aRetVal = mSystemLocales.Clone();
    return NS_OK;
  }

  if (ReadSystemLocales(aRetVal)) {
    mSystemLocales = aRetVal.Clone();
    return NS_OK;
  }

  // If we failed to get the system locale, we still need
  // to return something because there are tests out there that
  // depend on system locale to be set.
  aRetVal.AppendElement("en-US"_ns);
  return NS_ERROR_FAILURE;
}

NS_IMETHODIMP
OSPreferences::GetSystemLocale(nsACString& aRetVal) {
  if (!mSystemLocales.IsEmpty()) {
    aRetVal = mSystemLocales[0];
  } else {
    AutoTArray<nsCString, 10> locales;
    GetSystemLocales(locales);
    if (!locales.IsEmpty()) {
      aRetVal = locales[0];
    }
  }
  return NS_OK;
}

NS_IMETHODIMP
OSPreferences::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal) {
  if (!mRegionalPrefsLocales.IsEmpty()) {
    aRetVal = mRegionalPrefsLocales.Clone();
    return NS_OK;
  }

  if (ReadRegionalPrefsLocales(aRetVal)) {
    mRegionalPrefsLocales = aRetVal.Clone();
    return NS_OK;
  }

  // If we failed to read regional prefs locales,
  // use system locales as last fallback.
  return GetSystemLocales(aRetVal);
}

static OSPreferences::DateTimeFormatStyle ToDateTimeFormatStyle(
    int32_t aTimeFormat) {
  switch (aTimeFormat) {
    // See mozIOSPreferences.idl for the integer values here.
    case 0:
      return OSPreferences::DateTimeFormatStyle::None;
    case 1:
      return OSPreferences::DateTimeFormatStyle::Short;
    case 2:
      return OSPreferences::DateTimeFormatStyle::Medium;
    case 3:
      return OSPreferences::DateTimeFormatStyle::Long;
    case 4:
      return OSPreferences::DateTimeFormatStyle::Full;
  }
  return OSPreferences::DateTimeFormatStyle::Invalid;
}

NS_IMETHODIMP
OSPreferences::GetDateTimePattern(int32_t aDateFormatStyle,
                                  int32_t aTimeFormatStyle,
                                  const nsACString& aLocale,
                                  nsACString& aRetVal) {
  DateTimeFormatStyle dateStyle = ToDateTimeFormatStyle(aDateFormatStyle);
  if (dateStyle == DateTimeFormatStyle::Invalid) {
    return NS_ERROR_INVALID_ARG;
  }
  DateTimeFormatStyle timeStyle = ToDateTimeFormatStyle(aTimeFormatStyle);
  if (timeStyle == DateTimeFormatStyle::Invalid) {
    return NS_ERROR_INVALID_ARG;
  }

  // If the user is asking for None on both, date and time style,
  // let's exit early.
  if (timeStyle == DateTimeFormatStyle::None &&
      dateStyle == DateTimeFormatStyle::None) {
    return NS_OK;
  }

  // If the locale is not specified, default to first regional prefs locale
  const nsACString* locale = &aLocale;
  AutoTArray<nsCString, 10> rpLocales;
  if (aLocale.IsEmpty()) {
    LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
    MOZ_ASSERT(rpLocales.Length() > 0);
    locale = &rpLocales[0];
  }

  // Create a cache key from the locale + style options
  nsAutoCString key(*locale);
  key.Append(':');
  key.AppendInt(aDateFormatStyle);
  key.Append(':');
  key.AppendInt(aTimeFormatStyle);

  nsCString pattern;
  if (mPatternCache.Get(key, &pattern)) {
    aRetVal = std::move(pattern);
    return NS_OK;
  }

  if (!OverrideDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
    if (!ReadDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
      if (!GetDateTimePatternForStyle(dateStyle, timeStyle, *locale, pattern)) {
        return NS_ERROR_FAILURE;
      }
    }
  }

  if (mPatternCache.Count() == kMaxCachedPatterns) {
    // Don't allow unlimited cache growth; just throw it away in the case of
    // pathological behavior where a page keeps requesting different formats
    // and locales.
    NS_WARNING("flushing DateTimePattern cache");
    mPatternCache.Clear();
  }
  mPatternCache.InsertOrUpdate(key, pattern);

  aRetVal = std::move(pattern);
  return NS_OK;
}

void OSPreferences::OverrideSkeletonHourCycle(bool aIs24Hour,
                                              nsAutoCString& aSkeleton) {
  if (aIs24Hour) {
    // If aSkeleton contains 'h' or 'K', replace with 'H' or 'k' respectively,
    // and delete 'a' if present.
    if (aSkeleton.FindChar('h') == -1 && aSkeleton.FindChar('K') == -1) {
      return;
    }
    for (int32_t i = 0; i < int32_t(aSkeleton.Length()); ++i) {
      switch (aSkeleton[i]) {
        case 'a':
          aSkeleton.Cut(i, 1);
          --i;
          break;
        case 'h':
          aSkeleton.SetCharAt('H', i);
          break;
        case 'K':
          aSkeleton.SetCharAt('k', i);
          break;
      }
    }
  } else {
    // If skeleton contains 'H' or 'k', replace with 'h' or 'K' respectively,
    // and add 'a' unless already present.
    if (aSkeleton.FindChar('H') == -1 && aSkeleton.FindChar('k') == -1) {
      return;
    }
    bool foundA = false;
    for (size_t i = 0; i < aSkeleton.Length(); ++i) {
      switch (aSkeleton[i]) {
        case 'a':
          foundA = true;
          break;
        case 'H':
          aSkeleton.SetCharAt('h', i);
          break;
        case 'k':
          aSkeleton.SetCharAt('K', i);
          break;
      }
    }
    if (!foundA) {
      aSkeleton.Append(char16_t('a'));
    }
  }
}
