/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * 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/. */

/* Intl.NumberFormat implementation. */

#include "builtin/intl/NumberFormat.h"

#include "mozilla/Assertions.h"
#include "mozilla/Casting.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/intl/Locale.h"
#include "mozilla/intl/MeasureUnit.h"
#include "mozilla/intl/MeasureUnitGenerated.h"
#include "mozilla/intl/NumberFormat.h"
#include "mozilla/intl/NumberingSystem.h"
#include "mozilla/intl/NumberRangeFormat.h"
#include "mozilla/Span.h"
#include "mozilla/TextUtils.h"
#include "mozilla/UniquePtr.h"

#include <algorithm>
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <string_view>
#include <type_traits>

#include "builtin/Array.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/FormatBuffer.h"
#include "builtin/intl/LanguageTag.h"
#include "builtin/intl/RelativeTimeFormat.h"
#include "gc/GCContext.h"
#include "js/CharacterEncoding.h"
#include "js/PropertySpec.h"
#include "js/RootingAPI.h"
#include "js/TypeDecls.h"
#include "util/Text.h"
#include "vm/BigIntType.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
#include "vm/PlainObject.h"  // js::PlainObject
#include "vm/StringType.h"
#include "vm/WellKnownAtom.h"  // js_*_str

#include "vm/GeckoProfiler-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"

using namespace js;

using mozilla::AssertedCast;

using js::intl::DateTimeFormatOptions;
using js::intl::FieldType;

const JSClassOps NumberFormatObject::classOps_ = {
    nullptr,                       // addProperty
    nullptr,                       // delProperty
    nullptr,                       // enumerate
    nullptr,                       // newEnumerate
    nullptr,                       // resolve
    nullptr,                       // mayResolve
    NumberFormatObject::finalize,  // finalize
    nullptr,                       // call
    nullptr,                       // construct
    nullptr,                       // trace
};

const JSClass NumberFormatObject::class_ = {
    "Intl.NumberFormat",
    JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_NumberFormat) |
        JSCLASS_FOREGROUND_FINALIZE,
    &NumberFormatObject::classOps_, &NumberFormatObject::classSpec_};

const JSClass& NumberFormatObject::protoClass_ = PlainObject::class_;

static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  args.rval().setString(cx->names().NumberFormat);
  return true;
}

static const JSFunctionSpec numberFormat_static_methods[] = {
    JS_SELF_HOSTED_FN("supportedLocalesOf",
                      "Intl_NumberFormat_supportedLocalesOf", 1, 0),
    JS_FS_END,
};

static const JSFunctionSpec numberFormat_methods[] = {
    JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0,
                      0),
    JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0),
    JS_SELF_HOSTED_FN("formatRange", "Intl_NumberFormat_formatRange", 2, 0),
    JS_SELF_HOSTED_FN("formatRangeToParts",
                      "Intl_NumberFormat_formatRangeToParts", 2, 0),
    JS_FN(js_toSource_str, numberFormat_toSource, 0, 0),
    JS_FS_END,
};

static const JSPropertySpec numberFormat_properties[] = {
    JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0),
    JS_STRING_SYM_PS(toStringTag, "Intl.NumberFormat", JSPROP_READONLY),
    JS_PS_END,
};

static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp);

const ClassSpec NumberFormatObject::classSpec_ = {
    GenericCreateConstructor<NumberFormat, 0, gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<NumberFormatObject>,
    numberFormat_static_methods,
    nullptr,
    numberFormat_methods,
    numberFormat_properties,
    nullptr,
    ClassSpec::DontDefineConstructor};

/**
 * 15.1.1 Intl.NumberFormat ( [ locales [ , options ] ] )
 *
 * ES2024 Intl draft rev 74ca7099f103d143431b2ea422ae640c6f43e3e6
 */
static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) {
  AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.NumberFormat");

  // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).

  // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
  RootedObject proto(cx);
  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_NumberFormat,
                                          &proto)) {
    return false;
  }

  Rooted<NumberFormatObject*> numberFormat(cx);
  numberFormat = NewObjectWithClassProto<NumberFormatObject>(cx, proto);
  if (!numberFormat) {
    return false;
  }

  RootedValue thisValue(cx,
                        construct ? ObjectValue(*numberFormat) : args.thisv());
  HandleValue locales = args.get(0);
  HandleValue options = args.get(1);

  // Step 3.
  return intl::LegacyInitializeObject(
      cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales,
      options, DateTimeFormatOptions::Standard, args.rval());
}

static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return NumberFormat(cx, args, args.isConstructing());
}

bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 2);
  MOZ_ASSERT(!args.isConstructing());
  // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it
  // cannot be used with "new", but it still has to be treated as a
  // constructor.
  return NumberFormat(cx, args, true);
}

void js::NumberFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) {
  MOZ_ASSERT(gcx->onMainThread());

  auto* numberFormat = &obj->as<NumberFormatObject>();
  mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
  mozilla::intl::NumberRangeFormat* nrf =
      numberFormat->getNumberRangeFormatter();

  if (nf) {
    intl::RemoveICUCellMemory(gcx, obj, NumberFormatObject::EstimatedMemoryUse);
    // This was allocated using `new` in mozilla::intl::NumberFormat, so we
    // delete here.
    delete nf;
  }

  if (nrf) {
    intl::RemoveICUCellMemory(gcx, obj, EstimatedRangeFormatterMemoryUse);
    // This was allocated using `new` in mozilla::intl::NumberRangeFormat, so we
    // delete here.
    delete nrf;
  }
}

bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 1);
  MOZ_ASSERT(args[0].isString());

  UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
  if (!locale) {
    return false;
  }

  auto numberingSystem =
      mozilla::intl::NumberingSystem::TryCreate(locale.get());
  if (numberingSystem.isErr()) {
    intl::ReportInternalError(cx, numberingSystem.unwrapErr());
    return false;
  }

  auto name = numberingSystem.inspect()->GetName();
  if (name.isErr()) {
    intl::ReportInternalError(cx, name.unwrapErr());
    return false;
  }

  JSString* jsname = NewStringCopy<CanGC>(cx, name.unwrap());
  if (!jsname) {
    return false;
  }

  args.rval().setString(jsname);
  return true;
}

#if DEBUG || MOZ_SYSTEM_ICU
bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc,
                                        Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 0);

  RootedObject measurementUnits(cx, NewPlainObjectWithProto(cx, nullptr));
  if (!measurementUnits) {
    return false;
  }

  auto units = mozilla::intl::MeasureUnit::GetAvailable();
  if (units.isErr()) {
    intl::ReportInternalError(cx, units.unwrapErr());
    return false;
  }

  Rooted<JSAtom*> unitAtom(cx);
  for (auto unit : units.unwrap()) {
    if (unit.isErr()) {
      intl::ReportInternalError(cx);
      return false;
    }
    auto unitIdentifier = unit.unwrap();

    unitAtom = Atomize(cx, unitIdentifier.data(), unitIdentifier.size());
    if (!unitAtom) {
      return false;
    }

    if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(),
                            TrueHandleValue)) {
      return false;
    }
  }

  args.rval().setObject(*measurementUnits);
  return true;
}
#endif

static constexpr size_t MaxUnitLength() {
  size_t length = 0;
  for (const auto& unit : mozilla::intl::simpleMeasureUnits) {
    length = std::max(length, std::char_traits<char>::length(unit.name));
  }
  return length * 2 + std::char_traits<char>::length("-per-");
}

static UniqueChars NumberFormatLocale(JSContext* cx, HandleObject internals) {
  RootedValue value(cx);
  if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
    return nullptr;
  }

  // ICU expects numberingSystem as a Unicode locale extensions on locale.

  mozilla::intl::Locale tag;
  {
    Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx));
    if (!locale) {
      return nullptr;
    }

    if (!intl::ParseLocale(cx, locale, tag)) {
      return nullptr;
    }
  }

  JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);

  if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
                   &value)) {
    return nullptr;
  }

  {
    JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
    if (!numberingSystem) {
      return nullptr;
    }

    if (!keywords.emplaceBack("nu", numberingSystem)) {
      return nullptr;
    }
  }

  // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
  // the Unicode extension subtag. We're then relying on ICU to follow RFC
  // 6067, which states that any trailing keywords using the same key
  // should be ignored.
  if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
    return nullptr;
  }

  intl::FormatBuffer<char> buffer(cx);
  if (auto result = tag.ToString(buffer); result.isErr()) {
    intl::ReportInternalError(cx, result.unwrapErr());
    return nullptr;
  }
  return buffer.extractStringZ();
}

struct NumberFormatOptions : public mozilla::intl::NumberRangeFormatOptions {
  static_assert(std::is_base_of_v<mozilla::intl::NumberFormatOptions,
                                  mozilla::intl::NumberRangeFormatOptions>);

  char currencyChars[3] = {};
  char unitChars[MaxUnitLength()] = {};
};

static bool FillNumberFormatOptions(JSContext* cx, HandleObject internals,
                                    NumberFormatOptions& options) {
  RootedValue value(cx);
  if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
    return false;
  }

  bool accountingSign = false;
  {
    JSLinearString* style = value.toString()->ensureLinear(cx);
    if (!style) {
      return false;
    }

    if (StringEqualsLiteral(style, "currency")) {
      if (!GetProperty(cx, internals, internals, cx->names().currency,
                       &value)) {
        return false;
      }
      JSLinearString* currency = value.toString()->ensureLinear(cx);
      if (!currency) {
        return false;
      }

      MOZ_RELEASE_ASSERT(
          currency->length() == 3,
          "IsWellFormedCurrencyCode permits only length-3 strings");
      MOZ_ASSERT(StringIsAscii(currency),
                 "IsWellFormedCurrencyCode permits only ASCII strings");
      CopyChars(reinterpret_cast<Latin1Char*>(options.currencyChars),
                *currency);

      if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay,
                       &value)) {
        return false;
      }
      JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
      if (!currencyDisplay) {
        return false;
      }

      using CurrencyDisplay =
          mozilla::intl::NumberFormatOptions::CurrencyDisplay;

      CurrencyDisplay display;
      if (StringEqualsLiteral(currencyDisplay, "code")) {
        display = CurrencyDisplay::Code;
      } else if (StringEqualsLiteral(currencyDisplay, "symbol")) {
        display = CurrencyDisplay::Symbol;
      } else if (StringEqualsLiteral(currencyDisplay, "narrowSymbol")) {
        display = CurrencyDisplay::NarrowSymbol;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(currencyDisplay, "name"));
        display = CurrencyDisplay::Name;
      }

      if (!GetProperty(cx, internals, internals, cx->names().currencySign,
                       &value)) {
        return false;
      }
      JSLinearString* currencySign = value.toString()->ensureLinear(cx);
      if (!currencySign) {
        return false;
      }

      if (StringEqualsLiteral(currencySign, "accounting")) {
        accountingSign = true;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard"));
      }

      options.mCurrency = mozilla::Some(
          std::make_pair(std::string_view(options.currencyChars, 3), display));
    } else if (StringEqualsLiteral(style, "percent")) {
      options.mPercent = true;
    } else if (StringEqualsLiteral(style, "unit")) {
      if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) {
        return false;
      }
      JSLinearString* unit = value.toString()->ensureLinear(cx);
      if (!unit) {
        return false;
      }

      size_t unit_str_length = unit->length();

      MOZ_ASSERT(StringIsAscii(unit));
      MOZ_RELEASE_ASSERT(unit_str_length <= MaxUnitLength());
      CopyChars(reinterpret_cast<Latin1Char*>(options.unitChars), *unit);

      if (!GetProperty(cx, internals, internals, cx->names().unitDisplay,
                       &value)) {
        return false;
      }
      JSLinearString* unitDisplay = value.toString()->ensureLinear(cx);
      if (!unitDisplay) {
        return false;
      }

      using UnitDisplay = mozilla::intl::NumberFormatOptions::UnitDisplay;

      UnitDisplay display;
      if (StringEqualsLiteral(unitDisplay, "short")) {
        display = UnitDisplay::Short;
      } else if (StringEqualsLiteral(unitDisplay, "narrow")) {
        display = UnitDisplay::Narrow;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(unitDisplay, "long"));
        display = UnitDisplay::Long;
      }

      options.mUnit = mozilla::Some(std::make_pair(
          std::string_view(options.unitChars, unit_str_length), display));
    } else {
      MOZ_ASSERT(StringEqualsLiteral(style, "decimal"));
    }
  }

  bool hasMinimumSignificantDigits;
  if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
                   &hasMinimumSignificantDigits)) {
    return false;
  }

  if (hasMinimumSignificantDigits) {
    if (!GetProperty(cx, internals, internals,
                     cx->names().minimumSignificantDigits, &value)) {
      return false;
    }
    uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());

    if (!GetProperty(cx, internals, internals,
                     cx->names().maximumSignificantDigits, &value)) {
      return false;
    }
    uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());

    options.mSignificantDigits = mozilla::Some(
        std::make_pair(minimumSignificantDigits, maximumSignificantDigits));
  }

  bool hasMinimumFractionDigits;
  if (!HasProperty(cx, internals, cx->names().minimumFractionDigits,
                   &hasMinimumFractionDigits)) {
    return false;
  }

  if (hasMinimumFractionDigits) {
    if (!GetProperty(cx, internals, internals,
                     cx->names().minimumFractionDigits, &value)) {
      return false;
    }
    uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());

    if (!GetProperty(cx, internals, internals,
                     cx->names().maximumFractionDigits, &value)) {
      return false;
    }
    uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());

    options.mFractionDigits = mozilla::Some(
        std::make_pair(minimumFractionDigits, maximumFractionDigits));
  }

  if (!GetProperty(cx, internals, internals, cx->names().roundingPriority,
                   &value)) {
    return false;
  }

  {
    JSLinearString* roundingPriority = value.toString()->ensureLinear(cx);
    if (!roundingPriority) {
      return false;
    }

    using RoundingPriority =
        mozilla::intl::NumberFormatOptions::RoundingPriority;

    RoundingPriority priority;
    if (StringEqualsLiteral(roundingPriority, "auto")) {
      priority = RoundingPriority::Auto;
    } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) {
      priority = RoundingPriority::MorePrecision;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision"));
      priority = RoundingPriority::LessPrecision;
    }

    options.mRoundingPriority = priority;
  }

  if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
                   &value)) {
    return false;
  }
  options.mMinIntegerDigits =
      mozilla::Some(AssertedCast<uint32_t>(value.toInt32()));

  if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) {
    return false;
  }

  if (value.isString()) {
    JSLinearString* useGrouping = value.toString()->ensureLinear(cx);
    if (!useGrouping) {
      return false;
    }

    using Grouping = mozilla::intl::NumberFormatOptions::Grouping;

    Grouping grouping;
    if (StringEqualsLiteral(useGrouping, "auto")) {
      grouping = Grouping::Auto;
    } else if (StringEqualsLiteral(useGrouping, "always")) {
      grouping = Grouping::Always;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(useGrouping, "min2"));
      grouping = Grouping::Min2;
    }

    options.mGrouping = grouping;
  } else {
    MOZ_ASSERT(value.isBoolean());
    MOZ_ASSERT(value.toBoolean() == false);

    using Grouping = mozilla::intl::NumberFormatOptions::Grouping;

    options.mGrouping = Grouping::Never;
  }

  if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) {
    return false;
  }

  {
    JSLinearString* notation = value.toString()->ensureLinear(cx);
    if (!notation) {
      return false;
    }

    using Notation = mozilla::intl::NumberFormatOptions::Notation;

    Notation style;
    if (StringEqualsLiteral(notation, "standard")) {
      style = Notation::Standard;
    } else if (StringEqualsLiteral(notation, "scientific")) {
      style = Notation::Scientific;
    } else if (StringEqualsLiteral(notation, "engineering")) {
      style = Notation::Engineering;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(notation, "compact"));

      if (!GetProperty(cx, internals, internals, cx->names().compactDisplay,
                       &value)) {
        return false;
      }

      JSLinearString* compactDisplay = value.toString()->ensureLinear(cx);
      if (!compactDisplay) {
        return false;
      }

      if (StringEqualsLiteral(compactDisplay, "short")) {
        style = Notation::CompactShort;
      } else {
        MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long"));
        style = Notation::CompactLong;
      }
    }

    options.mNotation = style;
  }

  if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) {
    return false;
  }

  {
    JSLinearString* signDisplay = value.toString()->ensureLinear(cx);
    if (!signDisplay) {
      return false;
    }

    using SignDisplay = mozilla::intl::NumberFormatOptions::SignDisplay;

    SignDisplay display;
    if (StringEqualsLiteral(signDisplay, "auto")) {
      if (accountingSign) {
        display = SignDisplay::Accounting;
      } else {
        display = SignDisplay::Auto;
      }
    } else if (StringEqualsLiteral(signDisplay, "never")) {
      display = SignDisplay::Never;
    } else if (StringEqualsLiteral(signDisplay, "always")) {
      if (accountingSign) {
        display = SignDisplay::AccountingAlways;
      } else {
        display = SignDisplay::Always;
      }
    } else if (StringEqualsLiteral(signDisplay, "exceptZero")) {
      if (accountingSign) {
        display = SignDisplay::AccountingExceptZero;
      } else {
        display = SignDisplay::ExceptZero;
      }
    } else {
      MOZ_ASSERT(StringEqualsLiteral(signDisplay, "negative"));
      if (accountingSign) {
        display = SignDisplay::AccountingNegative;
      } else {
        display = SignDisplay::Negative;
      }
    }

    options.mSignDisplay = display;
  }

  if (!GetProperty(cx, internals, internals, cx->names().roundingIncrement,
                   &value)) {
    return false;
  }
  options.mRoundingIncrement = AssertedCast<uint32_t>(value.toInt32());

  if (!GetProperty(cx, internals, internals, cx->names().roundingMode,
                   &value)) {
    return false;
  }

  {
    JSLinearString* roundingMode = value.toString()->ensureLinear(cx);
    if (!roundingMode) {
      return false;
    }

    using RoundingMode = mozilla::intl::NumberFormatOptions::RoundingMode;

    RoundingMode rounding;
    if (StringEqualsLiteral(roundingMode, "halfExpand")) {
      // "halfExpand" is the default mode, so we handle it first.
      rounding = RoundingMode::HalfExpand;
    } else if (StringEqualsLiteral(roundingMode, "ceil")) {
      rounding = RoundingMode::Ceil;
    } else if (StringEqualsLiteral(roundingMode, "floor")) {
      rounding = RoundingMode::Floor;
    } else if (StringEqualsLiteral(roundingMode, "expand")) {
      rounding = RoundingMode::Expand;
    } else if (StringEqualsLiteral(roundingMode, "trunc")) {
      rounding = RoundingMode::Trunc;
    } else if (StringEqualsLiteral(roundingMode, "halfCeil")) {
      rounding = RoundingMode::HalfCeil;
    } else if (StringEqualsLiteral(roundingMode, "halfFloor")) {
      rounding = RoundingMode::HalfFloor;
    } else if (StringEqualsLiteral(roundingMode, "halfTrunc")) {
      rounding = RoundingMode::HalfTrunc;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(roundingMode, "halfEven"));
      rounding = RoundingMode::HalfEven;
    }

    options.mRoundingMode = rounding;
  }

  if (!GetProperty(cx, internals, internals, cx->names().trailingZeroDisplay,
                   &value)) {
    return false;
  }

  {
    JSLinearString* trailingZeroDisplay = value.toString()->ensureLinear(cx);
    if (!trailingZeroDisplay) {
      return false;
    }

    if (StringEqualsLiteral(trailingZeroDisplay, "auto")) {
      options.mStripTrailingZero = false;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(trailingZeroDisplay, "stripIfInteger"));
      options.mStripTrailingZero = true;
    }
  }

  return true;
}

/**
 * Returns a new mozilla::intl::Number[Range]Format with the locale and number
 * formatting options of the given NumberFormat, or a nullptr if
 * initialization failed.
 */
template <class Formatter>
static Formatter* NewNumberFormat(JSContext* cx,
                                  Handle<NumberFormatObject*> numberFormat) {
  RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat));
  if (!internals) {
    return nullptr;
  }

  UniqueChars locale = NumberFormatLocale(cx, internals);
  if (!locale) {
    return nullptr;
  }

  NumberFormatOptions options;
  if (!FillNumberFormatOptions(cx, internals, options)) {
    return nullptr;
  }

  options.mRangeCollapse = NumberFormatOptions::RangeCollapse::Auto;
  options.mRangeIdentityFallback =
      NumberFormatOptions::RangeIdentityFallback::Approximately;

  mozilla::Result<mozilla::UniquePtr<Formatter>, mozilla::intl::ICUError>
      result = Formatter::TryCreate(locale.get(), options);

  if (result.isOk()) {
    return result.unwrap().release();
  }

  intl::ReportInternalError(cx, result.unwrapErr());
  return nullptr;
}

static mozilla::intl::NumberFormat* GetOrCreateNumberFormat(
    JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
  // Obtain a cached mozilla::intl::NumberFormat object.
  mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
  if (nf) {
    return nf;
  }

  nf = NewNumberFormat<mozilla::intl::NumberFormat>(cx, numberFormat);
  if (!nf) {
    return nullptr;
  }
  numberFormat->setNumberFormatter(nf);

  intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedMemoryUse);
  return nf;
}

static mozilla::intl::NumberRangeFormat* GetOrCreateNumberRangeFormat(
    JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
  // Obtain a cached mozilla::intl::NumberRangeFormat object.
  mozilla::intl::NumberRangeFormat* nrf =
      numberFormat->getNumberRangeFormatter();
  if (nrf) {
    return nrf;
  }

  nrf = NewNumberFormat<mozilla::intl::NumberRangeFormat>(cx, numberFormat);
  if (!nrf) {
    return nullptr;
  }
  numberFormat->setNumberRangeFormatter(nrf);

  intl::AddICUCellMemory(numberFormat,
                         NumberFormatObject::EstimatedRangeFormatterMemoryUse);
  return nrf;
}

static FieldType GetFieldTypeForNumberPartType(
    mozilla::intl::NumberPartType type) {
  switch (type) {
    case mozilla::intl::NumberPartType::ApproximatelySign:
      return &JSAtomState::approximatelySign;
    case mozilla::intl::NumberPartType::Compact:
      return &JSAtomState::compact;
    case mozilla::intl::NumberPartType::Currency:
      return &JSAtomState::currency;
    case mozilla::intl::NumberPartType::Decimal:
      return &JSAtomState::decimal;
    case mozilla::intl::NumberPartType::ExponentInteger:
      return &JSAtomState::exponentInteger;
    case mozilla::intl::NumberPartType::ExponentMinusSign:
      return &JSAtomState::exponentMinusSign;
    case mozilla::intl::NumberPartType::ExponentSeparator:
      return &JSAtomState::exponentSeparator;
    case mozilla::intl::NumberPartType::Fraction:
      return &JSAtomState::fraction;
    case mozilla::intl::NumberPartType::Group:
      return &JSAtomState::group;
    case mozilla::intl::NumberPartType::Infinity:
      return &JSAtomState::infinity;
    case mozilla::intl::NumberPartType::Integer:
      return &JSAtomState::integer;
    case mozilla::intl::NumberPartType::Literal:
      return &JSAtomState::literal;
    case mozilla::intl::NumberPartType::MinusSign:
      return &JSAtomState::minusSign;
    case mozilla::intl::NumberPartType::Nan:
      return &JSAtomState::nan;
    case mozilla::intl::NumberPartType::Percent:
      return &JSAtomState::percentSign;
    case mozilla::intl::NumberPartType::PlusSign:
      return &JSAtomState::plusSign;
    case mozilla::intl::NumberPartType::Unit:
      return &JSAtomState::unit;
  }

  MOZ_ASSERT_UNREACHABLE(
      "unenumerated, undocumented format field returned by iterator");
  return nullptr;
}

static FieldType GetFieldTypeForNumberPartSource(
    mozilla::intl::NumberPartSource source) {
  switch (source) {
    case mozilla::intl::NumberPartSource::Shared:
      return &JSAtomState::shared;
    case mozilla::intl::NumberPartSource::Start:
      return &JSAtomState::startRange;
    case mozilla::intl::NumberPartSource::End:
      return &JSAtomState::endRange;
  }

  MOZ_CRASH("unexpected number part source");
}

enum class DisplayNumberPartSource : bool { No, Yes };

static bool FormattedNumberToParts(JSContext* cx, HandleString str,
                                   const mozilla::intl::NumberPartVector& parts,
                                   DisplayNumberPartSource displaySource,
                                   FieldType unitType,
                                   MutableHandleValue result) {
  size_t lastEndIndex = 0;

  RootedObject singlePart(cx);
  RootedValue propVal(cx);

  Rooted<ArrayObject*> partsArray(
      cx, NewDenseFullyAllocatedArray(cx, parts.length()));
  if (!partsArray) {
    return false;
  }
  partsArray->ensureDenseInitializedLength(0, parts.length());

  size_t index = 0;
  for (const auto& part : parts) {
    FieldType type = GetFieldTypeForNumberPartType(part.type);
    size_t endIndex = part.endIndex;

    MOZ_ASSERT(lastEndIndex < endIndex);

    singlePart = NewPlainObject(cx);
    if (!singlePart) {
      return false;
    }

    propVal.setString(cx->names().*type);
    if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) {
      return false;
    }

    JSLinearString* partSubstr =
        NewDependentString(cx, str, lastEndIndex, endIndex - lastEndIndex);
    if (!partSubstr) {
      return false;
    }

    propVal.setString(partSubstr);
    if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) {
      return false;
    }

    if (displaySource == DisplayNumberPartSource::Yes) {
      FieldType source = GetFieldTypeForNumberPartSource(part.source);

      propVal.setString(cx->names().*source);
      if (!DefineDataProperty(cx, singlePart, cx->names().source, propVal)) {
        return false;
      }
    }

    if (unitType != nullptr && type != &JSAtomState::literal) {
      propVal.setString(cx->names().*unitType);
      if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) {
        return false;
      }
    }

    partsArray->initDenseElement(index++, ObjectValue(*singlePart));

    lastEndIndex = endIndex;
  }

  MOZ_ASSERT(index == parts.length());
  MOZ_ASSERT(lastEndIndex == str->length(),
             "result array must partition the entire string");

  result.setObject(*partsArray);
  return true;
}

bool js::intl::FormattedRelativeTimeToParts(
    JSContext* cx, HandleString str,
    const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit,
    MutableHandleValue result) {
  return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No,
                                relativeTimeUnit, result);
}

// Return true if the string starts with "0[bBoOxX]", possibly skipping over
// leading whitespace.
template <typename CharT>
static bool IsNonDecimalNumber(mozilla::Range<const CharT> chars) {
  const CharT* end = chars.begin().get() + chars.length();
  const CharT* start = SkipSpace(chars.begin().get(), end);

  if (end - start >= 2 && start[0] == '0') {
    CharT ch = start[1];
    return ch == 'b' || ch == 'B' || ch == 'o' || ch == 'O' || ch == 'x' ||
           ch == 'X';
  }
  return false;
}

static bool IsNonDecimalNumber(JSLinearString* str) {
  JS::AutoCheckCannotGC nogc;
  return str->hasLatin1Chars() ? IsNonDecimalNumber(str->latin1Range(nogc))
                               : IsNonDecimalNumber(str->twoByteRange(nogc));
}

/**
 * 15.5.16 ToIntlMathematicalValue ( value )
 *
 * ES2024 Intl draft rev 74ca7099f103d143431b2ea422ae640c6f43e3e6
 */
static bool ToIntlMathematicalValue(JSContext* cx, MutableHandleValue value) {
  // Step 1.
  if (!ToPrimitive(cx, JSTYPE_NUMBER, value)) {
    return false;
  }

  // Step 2.
  if (value.isBigInt()) {
    return true;
  }

  // Step 4.
  if (!value.isString()) {
    // Step 4.a. (Steps 4.b-10 not applicable in our implementation.)
    return ToNumber(cx, value);
  }

  // Step 3.
  JSLinearString* str = value.toString()->ensureLinear(cx);
  if (!str) {
    return false;
  }

  // Steps 5-6, 8, and 9.a.
  double number = LinearStringToNumber(str);

  // Step 7.
  if (std::isnan(number)) {
    // Set to NaN if the input can't be parsed as a number.
    value.setNaN();
    return true;
  }

  // Step 9.
  if (number == 0.0 || std::isinf(number)) {
    // Step 9.a. (Reordered)

    // Steps 9.b-e.
    value.setDouble(number);
    return true;
  }

  // Step 10.
  if (IsNonDecimalNumber(str)) {
    // ICU doesn't accept non-decimal numbers, so we have to convert the input
    // into a base-10 string.

    MOZ_ASSERT(!mozilla::IsNegative(number),
               "non-decimal numbers can't be negative");

    if (number < DOUBLE_INTEGRAL_PRECISION_LIMIT) {
      // Fast-path if we can guarantee there was no loss of precision.
      value.setDouble(number);
    } else {
      // For the slow-path convert the string into a BigInt.

      // StringToBigInt can't fail (other than OOM) when StringToNumber already
      // succeeded.
      RootedString rooted(cx, str);
      BigInt* bi;
      JS_TRY_VAR_OR_RETURN_FALSE(cx, bi, StringToBigInt(cx, rooted));
      MOZ_ASSERT(bi);

      value.setBigInt(bi);
    }
  }
  return true;
}

// Return the number part of the input by removing leading and trailing
// whitespace.
template <typename CharT>
static mozilla::Span<const CharT> NumberPart(const CharT* chars,
                                             size_t length) {
  const CharT* start = chars;
  const CharT* end = chars + length;

  start = SkipSpace(start, end);

  // |SkipSpace| only supports forward iteration, so inline the backwards
  // iteration here.
  MOZ_ASSERT(start <= end);
  while (end > start && unicode::IsSpace(end[-1])) {
    end--;
  }

  // The number part is a non-empty, ASCII-only substring.
  MOZ_ASSERT(start < end);
  MOZ_ASSERT(mozilla::IsAscii(mozilla::Span(start, end)));

  return {start, end};
}

static bool NumberPart(JSContext* cx, JSLinearString* str,
                       const JS::AutoCheckCannotGC& nogc,
                       JS::UniqueChars& latin1, std::string_view& result) {
  if (str->hasLatin1Chars()) {
    auto span = NumberPart(
        reinterpret_cast<const char*>(str->latin1Chars(nogc)), str->length());

    result = {span.data(), span.size()};
    return true;
  }

  auto span = NumberPart(str->twoByteChars(nogc), str->length());

  latin1.reset(JS::LossyTwoByteCharsToNewLatin1CharsZ(cx, span).c_str());
  if (!latin1) {
    return false;
  }

  result = {latin1.get(), span.size()};
  return true;
}

bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 3);
  MOZ_ASSERT(args[0].isObject());
  MOZ_ASSERT(args[2].isBoolean());

  Rooted<NumberFormatObject*> numberFormat(
      cx, &args[0].toObject().as<NumberFormatObject>());

  RootedValue value(cx, args[1]);
  if (!ToIntlMathematicalValue(cx, &value)) {
    return false;
  }

  mozilla::intl::NumberFormat* nf = GetOrCreateNumberFormat(cx, numberFormat);
  if (!nf) {
    return false;
  }

  // Actually format the number
  using ICUError = mozilla::intl::ICUError;

  bool formatToParts = args[2].toBoolean();
  mozilla::Result<std::u16string_view, ICUError> result =
      mozilla::Err(ICUError::InternalError);
  mozilla::intl::NumberPartVector parts;
  if (value.isNumber()) {
    double num = value.toNumber();
    if (formatToParts) {
      result = nf->formatToParts(num, parts);
    } else {
      result = nf->format(num);
    }
  } else if (value.isBigInt()) {
    RootedBigInt bi(cx, value.toBigInt());

    int64_t num;
    if (BigInt::isInt64(bi, &num)) {
      if (formatToParts) {
        result = nf->formatToParts(num, parts);
      } else {
        result = nf->format(num);
      }
    } else {
      JSLinearString* str = BigInt::toString<CanGC>(cx, bi, 10);
      if (!str) {
        return false;
      }
      MOZ_RELEASE_ASSERT(str->hasLatin1Chars());

      JS::AutoCheckCannotGC nogc;

      const char* chars = reinterpret_cast<const char*>(str->latin1Chars(nogc));
      if (formatToParts) {
        result =
            nf->formatToParts(std::string_view(chars, str->length()), parts);
      } else {
        result = nf->format(std::string_view(chars, str->length()));
      }
    }
  } else {
    JSLinearString* str = value.toString()->ensureLinear(cx);
    if (!str) {
      return false;
    }

    JS::AutoCheckCannotGC nogc;

    // Two-byte strings have to be copied into a separate |char| buffer.
    JS::UniqueChars latin1;

    std::string_view sv;
    if (!NumberPart(cx, str, nogc, latin1, sv)) {
      return false;
    }

    if (formatToParts) {
      result = nf->formatToParts(sv, parts);
    } else {
      result = nf->format(sv);
    }
  }

  if (result.isErr()) {
    intl::ReportInternalError(cx, result.unwrapErr());
    return false;
  }

  RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
  if (!str) {
    return false;
  }

  if (formatToParts) {
    return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No,
                                  nullptr, args.rval());
  }

  args.rval().setString(str);
  return true;
}

static JSLinearString* ToLinearString(JSContext* cx, HandleValue val) {
  // Special case to preserve negative zero.
  if (val.isDouble() && mozilla::IsNegativeZero(val.toDouble())) {
    constexpr std::string_view negativeZero = "-0";
    return NewStringCopy<CanGC>(cx, negativeZero);
  }

  JSString* str = ToString(cx, val);
  return str ? str->ensureLinear(cx) : nullptr;
};

bool js::intl_FormatNumberRange(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 4);
  MOZ_ASSERT(args[0].isObject());
  MOZ_ASSERT(!args[1].isUndefined());
  MOZ_ASSERT(!args[2].isUndefined());
  MOZ_ASSERT(args[3].isBoolean());

  Rooted<NumberFormatObject*> numberFormat(
      cx, &args[0].toObject().as<NumberFormatObject>());
  bool formatToParts = args[3].toBoolean();

  RootedValue start(cx, args[1]);
  if (!ToIntlMathematicalValue(cx, &start)) {
    return false;
  }

  RootedValue end(cx, args[2]);
  if (!ToIntlMathematicalValue(cx, &end)) {
    return false;
  }

  // PartitionNumberRangePattern, step 1.
  if (start.isDouble() && std::isnan(start.toDouble())) {
    JS_ReportErrorNumberASCII(
        cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start",
        "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange");
    return false;
  }
  if (end.isDouble() && std::isnan(end.toDouble())) {
    JS_ReportErrorNumberASCII(
        cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "end",
        "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange");
    return false;
  }

  using NumberRangeFormat = mozilla::intl::NumberRangeFormat;
  NumberRangeFormat* nf = GetOrCreateNumberRangeFormat(cx, numberFormat);
  if (!nf) {
    return false;
  }

  auto valueRepresentableAsDouble = [](const Value& val, double* num) {
    if (val.isNumber()) {
      *num = val.toNumber();
      return true;
    }
    if (val.isBigInt()) {
      int64_t i64;
      if (BigInt::isInt64(val.toBigInt(), &i64) &&
          i64 < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) &&
          i64 > -int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) {
        *num = double(i64);
        return true;
      }
    }
    return false;
  };

  // Actually format the number range.
  using ICUError = mozilla::intl::ICUError;

  mozilla::Result<std::u16string_view, ICUError> result =
      mozilla::Err(ICUError::InternalError);
  mozilla::intl::NumberPartVector parts;

  double numStart, numEnd;
  if (valueRepresentableAsDouble(start, &numStart) &&
      valueRepresentableAsDouble(end, &numEnd)) {
    if (formatToParts) {
      result = nf->formatToParts(numStart, numEnd, parts);
    } else {
      result = nf->format(numStart, numEnd);
    }
  } else {
    Rooted<JSLinearString*> strStart(cx, ToLinearString(cx, start));
    if (!strStart) {
      return false;
    }

    Rooted<JSLinearString*> strEnd(cx, ToLinearString(cx, end));
    if (!strEnd) {
      return false;
    }

    JS::AutoCheckCannotGC nogc;

    // Two-byte strings have to be copied into a separate |char| buffer.
    JS::UniqueChars latin1Start;
    JS::UniqueChars latin1End;

    std::string_view svStart;
    if (!NumberPart(cx, strStart, nogc, latin1Start, svStart)) {
      return false;
    }

    std::string_view svEnd;
    if (!NumberPart(cx, strEnd, nogc, latin1End, svEnd)) {
      return false;
    }

    if (formatToParts) {
      result = nf->formatToParts(svStart, svEnd, parts);
    } else {
      result = nf->format(svStart, svEnd);
    }
  }

  if (result.isErr()) {
    intl::ReportInternalError(cx, result.unwrapErr());
    return false;
  }

  RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
  if (!str) {
    return false;
  }

  if (formatToParts) {
    return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::Yes,
                                  nullptr, args.rval());
  }

  args.rval().setString(str);
  return true;
}
