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

/* Portions Copyright Norbert Lindenberg 2011-2012. */

/**
 * 11.1.2 CreateDateTimeFormat ( newTarget, locales, options, required, defaults )
 *
 * Compute an internal properties object from |lazyDateTimeFormatData|.
 */
function resolveDateTimeFormatInternals(lazyDateTimeFormatData) {
  assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?");

  // Lazy DateTimeFormat data has the following structure:
  //
  //   {
  //     requestedLocales: List of locales,
  //
  //     localeOpt: // *first* opt computed in InitializeDateTimeFormat
  //       {
  //         localeMatcher: "lookup" / "best fit",
  //
  //         ca: string matching a Unicode extension type, // optional
  //
  //         nu: string matching a Unicode extension type, // optional
  //
  //         hc: "h11" / "h12" / "h23" / "h24", // optional
  //       }
  //
  //     timeZone: IANA time zone name or a normalized time zone offset string,
  //
  //     formatOptions: // *second* opt computed in InitializeDateTimeFormat
  //       {
  //         // all the properties/values listed in Table 3
  //         // (weekday, era, year, month, day, &c.)
  //
  //         hour12: true / false,  // optional
  //       }
  //
  //     formatMatcher: "basic" / "best fit",
  //
  //     dateStyle: "full" / "long" / "medium" / "short" / undefined,
  //
  //     timeStyle: "full" / "long" / "medium" / "short" / undefined,
  //
  //     patternOption:
  //       String representing LDML Date Format pattern or undefined
  //   }
  //
  // Note that lazy data is only installed as a final step of initialization,
  // so every DateTimeFormat lazy data object has *all* these properties,
  // never a subset of them.

  var internalProps = std_Object_create(null);

  var DateTimeFormat = dateTimeFormatInternalProperties;

  // Compute effective locale.

  // Step 17.
  var localeData = DateTimeFormat.localeData;

  // Step 18.
  var r = ResolveLocale(
    "DateTimeFormat",
    lazyDateTimeFormatData.requestedLocales,
    lazyDateTimeFormatData.localeOpt,
    DateTimeFormat.relevantExtensionKeys,
    localeData
  );

  // Steps 19-22.
  internalProps.locale = r.locale;
  internalProps.calendar = r.ca;
  internalProps.numberingSystem = r.nu;

  // Step 34. (Reordered)
  var formatOptions = lazyDateTimeFormatData.formatOptions;

  // Steps 23-29.
  //
  // Copy the hourCycle setting, if present, to the format options. But
  // only do this if no hour12 option is present, because the latter takes
  // precedence over hourCycle.
  if (r.hc !== null && formatOptions.hour12 === undefined) {
    formatOptions.hourCycle = r.hc;
  }

  // Step 33.
  internalProps.timeZone = lazyDateTimeFormatData.timeZone;

  // Steps 45-50, more or less.
  if (lazyDateTimeFormatData.patternOption !== undefined) {
    internalProps.pattern = lazyDateTimeFormatData.patternOption;
  } else if (
    lazyDateTimeFormatData.dateStyle !== undefined ||
    lazyDateTimeFormatData.timeStyle !== undefined
  ) {
    internalProps.hourCycle = formatOptions.hourCycle;
    internalProps.hour12 = formatOptions.hour12;
    internalProps.dateStyle = lazyDateTimeFormatData.dateStyle;
    internalProps.timeStyle = lazyDateTimeFormatData.timeStyle;
  } else {
    internalProps.hourCycle = formatOptions.hourCycle;
    internalProps.hour12 = formatOptions.hour12;
    internalProps.weekday = formatOptions.weekday;
    internalProps.era = formatOptions.era;
    internalProps.year = formatOptions.year;
    internalProps.month = formatOptions.month;
    internalProps.day = formatOptions.day;
    internalProps.dayPeriod = formatOptions.dayPeriod;
    internalProps.hour = formatOptions.hour;
    internalProps.minute = formatOptions.minute;
    internalProps.second = formatOptions.second;
    internalProps.fractionalSecondDigits = formatOptions.fractionalSecondDigits;
    internalProps.timeZoneName = formatOptions.timeZoneName;
  }

  // The caller is responsible for associating |internalProps| with the right
  // object using |setInternalProperties|.
  return internalProps;
}

/**
 * Returns an object containing the DateTimeFormat internal properties of |obj|.
 */
function getDateTimeFormatInternals(obj) {
  assert(IsObject(obj), "getDateTimeFormatInternals called with non-object");
  assert(
    intl_GuardToDateTimeFormat(obj) !== null,
    "getDateTimeFormatInternals called with non-DateTimeFormat"
  );

  var internals = getIntlObjectInternals(obj);
  assert(
    internals.type === "DateTimeFormat",
    "bad type escaped getIntlObjectInternals"
  );

  // If internal properties have already been computed, use them.
  var internalProps = maybeInternalProperties(internals);
  if (internalProps) {
    return internalProps;
  }

  // Otherwise it's time to fully create them.
  internalProps = resolveDateTimeFormatInternals(internals.lazyData);
  setInternalProperties(internals, internalProps);
  return internalProps;
}

/**
 * 12.1.10 UnwrapDateTimeFormat( dtf )
 */
function UnwrapDateTimeFormat(dtf) {
  // Steps 2 and 4 (error handling moved to caller).
  if (
    IsObject(dtf) &&
    intl_GuardToDateTimeFormat(dtf) === null &&
    !intl_IsWrappedDateTimeFormat(dtf) &&
    callFunction(
      std_Object_isPrototypeOf,
      GetBuiltinPrototype("DateTimeFormat"),
      dtf
    )
  ) {
    dtf = dtf[intlFallbackSymbol()];
  }
  return dtf;
}

/**
 * 6.4.2 CanonicalizeTimeZoneName ( timeZone )
 *
 * Canonicalizes the given IANA time zone name.
 *
 * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3
 */
function CanonicalizeTimeZoneName(timeZone) {
  assert(typeof timeZone === "string", "CanonicalizeTimeZoneName");

  // Step 1. (Not applicable, the input is already a valid IANA time zone.)
  assert(timeZone !== "Etc/Unknown", "Invalid time zone");
  assert(
    timeZone === intl_IsValidTimeZoneName(timeZone),
    "Time zone name not normalized"
  );

  // Step 2.
  var ianaTimeZone = intl_canonicalizeTimeZone(timeZone);
  assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone");
  assert(
    ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone),
    "Unsupported canonical time zone"
  );

  // Step 3.
  if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") {
    ianaTimeZone = "UTC";
  }

  // Step 4.
  return ianaTimeZone;
}

var timeZoneCache = {
  icuDefaultTimeZone: undefined,
  defaultTimeZone: undefined,
};

/**
 * 6.4.3 DefaultTimeZone ()
 *
 * Returns the IANA time zone name for the host environment's current time zone.
 *
 * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3
 */
function DefaultTimeZone() {
  if (intl_isDefaultTimeZone(timeZoneCache.icuDefaultTimeZone)) {
    return timeZoneCache.defaultTimeZone;
  }

  // Verify that the current ICU time zone is a valid ECMA-402 time zone.
  var icuDefaultTimeZone = intl_defaultTimeZone();
  var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone);
  if (timeZone === null) {
    // Before defaulting to "UTC", try to represent the default time zone
    // using the Etc/GMT + offset format. This format only accepts full
    // hour offsets.
    var msPerHour = 60 * 60 * 1000;
    var offset = intl_defaultTimeZoneOffset();
    assert(
      offset === (offset | 0),
      "milliseconds offset shouldn't be able to exceed int32_t range"
    );
    var offsetHours = offset / msPerHour;
    var offsetHoursFraction = offset % msPerHour;
    if (offsetHoursFraction === 0) {
      // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset
      // means a location west of GMT.
      timeZone =
        "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours);

      // Check if the fallback is valid.
      timeZone = intl_IsValidTimeZoneName(timeZone);
    }

    // Fallback to "UTC" if everything else fails.
    if (timeZone === null) {
      timeZone = "UTC";
    }
  }

  // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC.
  var defaultTimeZone = CanonicalizeTimeZoneName(timeZone);

  timeZoneCache.defaultTimeZone = defaultTimeZone;
  timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone;

  return defaultTimeZone;
}

/**
 * 21.4.1.33.1 IsTimeZoneOffsetString ( offsetString )
 * 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString )
 * 11.1.3 FormatOffsetTimeZoneIdentifier ( offsetMinutes )
 *
 * Function to parse, validate, and normalize time zone offset strings.
 *
 * ES2024 draft rev 10d44bfce4640894a0ed366bb769f2700cc8839a
 * ES2024 Intl draft rev 2f002b2000bf8b908efb793767bcfd23620e06db
 */
function TimeZoneOffsetString(offsetString) {
  assert(typeof(offsetString) === "string", "offsetString is a string");

  // UTCOffset :::
  //   TemporalSign Hour
  //   TemporalSign Hour HourSubcomponents[+Extended]
  //   TemporalSign Hour HourSubcomponents[~Extended]
  //
  // TemporalSign :::
  //   ASCIISign
  //   <MINUS>
  //
  // With <MINUS> = U+2212
  //
  // ASCIISign ::: one of
  //   + -
  //
  // Hour :::
  //   0 DecimalDigit
  //   1 DecimalDigit
  //   20
  //   21
  //   22
  //   23
  //
  // HourSubcomponents[Extended] :::
  //   TimeSeparator[?Extended] MinuteSecond
  //
  // TimeSeparator[Extended] :::
  //   [+Extended] :
  //   [~Extended] [empty]
  //
  // MinuteSecond :::
  //   0 DecimalDigit
  //   1 DecimalDigit
  //   2 DecimalDigit
  //   3 DecimalDigit
  //   4 DecimalDigit
  //   5 DecimalDigit

  // Return if there are too few or too many characters for an offset string.
  if (offsetString.length < 3 || offsetString.length > 6) {
    return null;
  }

  // Self-hosted code only supports Latin-1 permanent atoms, so the Unicode <MINUS>
  // can't be used in a string literal "\u2212". That means the first character has
  // to be checked using the character code instead of performing a normal string
  // comparison. Alternatively <MINUS> could be generated at runtime through
  // |std_String_fromCharCode(0x2212)|, but that means allocating a string just for
  // the comparison. And for consistency also check the remaining characters through
  // their character code.

  #define PLUS_SIGN 0x2b
  #define HYPHEN_MINUS 0x2d
  #define MINUS 0x2212
  #define COLON 0x3a
  #define DIGIT_ZERO 0x30
  #define DIGIT_TWO 0x32
  #define DIGIT_THREE 0x33
  #define DIGIT_FIVE 0x35
  #define DIGIT_NINE 0x39

  /* global PLUS_SIGN, HYPHEN_MINUS, MINUS, COLON */
  /* global DIGIT_ZERO, DIGIT_TWO, DIGIT_THREE, DIGIT_FIVE, DIGIT_NINE */

  // The first character must match |TemporalSign|.
  var sign = callFunction(std_String_charCodeAt, offsetString, 0);
  if (sign !== PLUS_SIGN && sign !== HYPHEN_MINUS && sign !== MINUS) {
    return null;
  }

  // Read the next two characters for the |Hour| grammar production.
  var hourTens = callFunction(std_String_charCodeAt, offsetString, 1);
  var hourOnes = callFunction(std_String_charCodeAt, offsetString, 2);

  // Read the remaining characters for the optional |MinuteSecond| grammar production.
  var minutesTens = DIGIT_ZERO;
  var minutesOnes = DIGIT_ZERO;
  if (offsetString.length > 3) {
    // |TimeSeparator| is optional.
    var separatorLength = offsetString[3] === ":" ? 1 : 0;

    // Return if there are too many characters for an offset string.
    if (offsetString.length !== (5 + separatorLength)) {
      return null;
    }

    minutesTens = callFunction(
      std_String_charCodeAt,
      offsetString,
      3 + separatorLength,
    );
    minutesOnes = callFunction(
      std_String_charCodeAt,
      offsetString,
      4 + separatorLength,
    );
  }

  // Validate the characters match the |Hour| and |MinuteSecond| productions:
  // - hours must be in the range 0..23
  // - minutes must in the range 0..59
  if (
    hourTens < DIGIT_ZERO ||
    hourOnes < DIGIT_ZERO ||
    minutesTens < DIGIT_ZERO ||
    minutesOnes < DIGIT_ZERO ||
    hourTens > DIGIT_TWO ||
    hourOnes > DIGIT_NINE ||
    minutesTens > DIGIT_FIVE ||
    minutesOnes > DIGIT_NINE ||
    (hourTens === DIGIT_TWO && hourOnes > DIGIT_THREE)
  ) {
    return null;
  }

  // FormatOffsetTimeZoneIdentifier, steps 1-5.
  if (
    hourTens === DIGIT_ZERO &&
    hourOnes === DIGIT_ZERO &&
    minutesTens === DIGIT_ZERO &&
    minutesOnes === DIGIT_ZERO
  ) {
    sign = PLUS_SIGN;
  } else if (sign === MINUS) {
    sign = HYPHEN_MINUS;
  }

  return std_String_fromCharCode(
    sign,
    hourTens,
    hourOnes,
    COLON,
    minutesTens,
    minutesOnes,
  );

  #undef PLUS_SIGN
  #undef HYPHEN_MINUS
  #undef MINUS
  #undef COLON
  #undef DIGIT_ZERO
  #undef DIGIT_TWO
  #undef DIGIT_THREE
  #undef DIGIT_FIVE
  #undef DIGIT_NINE
}

/* eslint-disable complexity */
/**
 * 11.1.2 CreateDateTimeFormat ( newTarget, locales, options, required, defaults )
 *
 * Initializes an object as a DateTimeFormat.
 *
 * This method is complicated a moderate bit by its implementing initialization
 * as a *lazy* concept.  Everything that must happen now, does -- but we defer
 * all the work we can until the object is actually used as a DateTimeFormat.
 * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted
 * here occur there.
 */
function InitializeDateTimeFormat(
  dateTimeFormat,
  thisValue,
  locales,
  options,
  required,
  defaults,
  mozExtensions
) {
  assert(
    IsObject(dateTimeFormat),
    "InitializeDateTimeFormat called with non-Object"
  );
  assert(
    intl_GuardToDateTimeFormat(dateTimeFormat) !== null,
    "InitializeDateTimeFormat called with non-DateTimeFormat"
  );
  assert(
    required === "date" || required === "time" || required === "any",
    `InitializeDateTimeFormat called with invalid required value: ${required}`
  );
  assert(
    defaults === "date" || defaults === "time" || defaults === "all",
    `InitializeDateTimeFormat called with invalid defaults value: ${defaults}`
  );

  // Lazy DateTimeFormat data has the following structure:
  //
  //   {
  //     requestedLocales: List of locales,
  //
  //     localeOpt: // *first* opt computed in InitializeDateTimeFormat
  //       {
  //         localeMatcher: "lookup" / "best fit",
  //
  //         ca: string matching a Unicode extension type, // optional
  //
  //         nu: string matching a Unicode extension type, // optional
  //
  //         hc: "h11" / "h12" / "h23" / "h24", // optional
  //       }
  //
  //     timeZone: IANA time zone name or a normalized time zone offset string,
  //
  //     formatOptions: // *second* opt computed in InitializeDateTimeFormat
  //       {
  //         // all the properties/values listed in Table 3
  //         // (weekday, era, year, month, day, &c.)
  //
  //         hour12: true / false,  // optional
  //       }
  //
  //     formatMatcher: "basic" / "best fit",
  //   }
  //
  // Note that lazy data is only installed as a final step of initialization,
  // so every DateTimeFormat lazy data object has *all* these properties,
  // never a subset of them.
  var lazyDateTimeFormatData = std_Object_create(null);

  // Step 1. (Performed in caller)

  // Step 2.
  var requestedLocales = CanonicalizeLocaleList(locales);
  lazyDateTimeFormatData.requestedLocales = requestedLocales;

  // Step 3. (Inlined call to CoerceOptionsToObject.)
  if (options === undefined) {
    options = std_Object_create(null);
  } else {
    options = ToObject(options);
  }

  // Compute options that impact interpretation of locale.
  // Step 4.
  var localeOpt = new_Record();
  lazyDateTimeFormatData.localeOpt = localeOpt;

  // Steps 5-6.
  var localeMatcher = GetOption(
    options,
    "localeMatcher",
    "string",
    ["lookup", "best fit"],
    "best fit"
  );
  localeOpt.localeMatcher = localeMatcher;

  // Step 7.
  var calendar = GetOption(options, "calendar", "string", undefined, undefined);

  // Step 8.
  if (calendar !== undefined) {
    calendar = intl_ValidateAndCanonicalizeUnicodeExtensionType(
      calendar,
      "calendar",
      "ca"
    );
  }

  // Step 9.
  localeOpt.ca = calendar;

  // Step 10.
  var numberingSystem = GetOption(
    options,
    "numberingSystem",
    "string",
    undefined,
    undefined
  );

  // Step 11.
  if (numberingSystem !== undefined) {
    numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType(
      numberingSystem,
      "numberingSystem",
      "nu"
    );
  }

  // Step 12.
  localeOpt.nu = numberingSystem;

  // Step 13.
  var hour12 = GetOption(options, "hour12", "boolean", undefined, undefined);

  // Step 14.
  var hourCycle = GetOption(
    options,
    "hourCycle",
    "string",
    ["h11", "h12", "h23", "h24"],
    undefined
  );

  // Step 15.
  if (hour12 !== undefined) {
    // The "hourCycle" option is ignored if "hr12" is also present.
    hourCycle = null;
  }

  // Step 16.
  localeOpt.hc = hourCycle;

  // Steps 17-29 (see resolveDateTimeFormatInternals).

  // Step 29.
  var timeZone = options.timeZone;

  // Steps 30-34.
  if (timeZone === undefined) {
    // Step 30.a.
    timeZone = DefaultTimeZone();

    // Steps 32-34. (Not applicable in our implementation.)
  } else {
    // Step 31.a.
    timeZone = ToString(timeZone);

    // Steps 32-34.
    var offsetString = TimeZoneOffsetString(timeZone);
    if (offsetString !== null) {
      // Steps 32.a-g. (Performed in TimeZoneOffsetString in our implementation.)
      timeZone = offsetString;
    } else {
      // Steps 33-34.
      var validTimeZone = intl_IsValidTimeZoneName(timeZone);
      if (validTimeZone !== null) {
        // Step 33.a.
        timeZone = CanonicalizeTimeZoneName(validTimeZone);
      } else {
        // Step 34.a.
        ThrowRangeError(JSMSG_INVALID_TIME_ZONE, timeZone);
      }
    }
  }

  // Step 33.
  lazyDateTimeFormatData.timeZone = timeZone;

  // Step 34.
  var formatOptions = new_Record();
  lazyDateTimeFormatData.formatOptions = formatOptions;

  if (mozExtensions) {
    var pattern = GetOption(options, "pattern", "string", undefined, undefined);
    lazyDateTimeFormatData.patternOption = pattern;
  }

  // Step 35.
  //
  // Pass hr12 on to ICU. The hour cycle option is passed through |localeOpt|.
  if (hour12 !== undefined) {
    formatOptions.hour12 = hour12;
  }

  // Step 36. (Explicit format component computed in step 43.)

  // Step 37.
  // 11.5, Table 7: Components of date and time formats.
  formatOptions.weekday = GetOption(
    options,
    "weekday",
    "string",
    ["narrow", "short", "long"],
    undefined
  );
  formatOptions.era = GetOption(
    options,
    "era",
    "string",
    ["narrow", "short", "long"],
    undefined
  );
  formatOptions.year = GetOption(
    options,
    "year",
    "string",
    ["2-digit", "numeric"],
    undefined
  );
  formatOptions.month = GetOption(
    options,
    "month",
    "string",
    ["2-digit", "numeric", "narrow", "short", "long"],
    undefined
  );
  formatOptions.day = GetOption(
    options,
    "day",
    "string",
    ["2-digit", "numeric"],
    undefined
  );
  formatOptions.dayPeriod = GetOption(
    options,
    "dayPeriod",
    "string",
    ["narrow", "short", "long"],
    undefined
  );
  formatOptions.hour = GetOption(
    options,
    "hour",
    "string",
    ["2-digit", "numeric"],
    undefined
  );
  formatOptions.minute = GetOption(
    options,
    "minute",
    "string",
    ["2-digit", "numeric"],
    undefined
  );
  formatOptions.second = GetOption(
    options,
    "second",
    "string",
    ["2-digit", "numeric"],
    undefined
  );
  formatOptions.fractionalSecondDigits = GetNumberOption(
    options,
    "fractionalSecondDigits",
    1,
    3,
    undefined
  );
  formatOptions.timeZoneName = GetOption(
    options,
    "timeZoneName",
    "string",
    [
      "short",
      "long",
      "shortOffset",
      "longOffset",
      "shortGeneric",
      "longGeneric",
    ],
    undefined
  );

  // Step 38.
  //
  // For some reason (ICU not exposing enough interface?) we drop the
  // requested format matcher on the floor after this.  In any case, even if
  // doing so is justified, we have to do this work here in case it triggers
  // getters or similar. (bug 852837)
  var formatMatcher = GetOption(
    options,
    "formatMatcher",
    "string",
    ["basic", "best fit"],
    "best fit"
  );
  void formatMatcher;

  // Steps 39-40.
  var dateStyle = GetOption(
    options,
    "dateStyle",
    "string",
    ["full", "long", "medium", "short"],
    undefined
  );
  lazyDateTimeFormatData.dateStyle = dateStyle;

  // Steps 41-42.
  var timeStyle = GetOption(
    options,
    "timeStyle",
    "string",
    ["full", "long", "medium", "short"],
    undefined
  );
  lazyDateTimeFormatData.timeStyle = timeStyle;

  // Step 43.
  if (dateStyle !== undefined || timeStyle !== undefined) {
    /* eslint-disable no-nested-ternary */
    var explicitFormatComponent =
      formatOptions.weekday !== undefined
        ? "weekday"
        : formatOptions.era !== undefined
        ? "era"
        : formatOptions.year !== undefined
        ? "year"
        : formatOptions.month !== undefined
        ? "month"
        : formatOptions.day !== undefined
        ? "day"
        : formatOptions.dayPeriod !== undefined
        ? "dayPeriod"
        : formatOptions.hour !== undefined
        ? "hour"
        : formatOptions.minute !== undefined
        ? "minute"
        : formatOptions.second !== undefined
        ? "second"
        : formatOptions.fractionalSecondDigits !== undefined
        ? "fractionalSecondDigits"
        : formatOptions.timeZoneName !== undefined
        ? "timeZoneName"
        : undefined;
    /* eslint-enable no-nested-ternary */

    // Step 43.a.
    if (explicitFormatComponent !== undefined) {
      ThrowTypeError(
        JSMSG_INVALID_DATETIME_OPTION,
        explicitFormatComponent,
        dateStyle !== undefined ? "dateStyle" : "timeStyle"
      );
    }

    // Step 43.b.
    if (required === "date" && timeStyle !== undefined) {
      ThrowTypeError(
        JSMSG_INVALID_DATETIME_STYLE,
        "timeStyle",
        "toLocaleDateString"
      );
    }

    // Step 43.c.
    if (required === "time" && dateStyle !== undefined) {
      ThrowTypeError(
        JSMSG_INVALID_DATETIME_STYLE,
        "dateStyle",
        "toLocaleTimeString"
      );
    }
  } else {
    // Step 44.a.
    var needDefaults = true;

    // Step 44.b.
    if (required === "date" || required === "any") {
      needDefaults =
        formatOptions.weekday === undefined &&
        formatOptions.year === undefined &&
        formatOptions.month === undefined &&
        formatOptions.day === undefined;
    }

    // Step 44.c.
    if (required === "time" || required === "any") {
      needDefaults =
        needDefaults &&
        formatOptions.dayPeriod === undefined &&
        formatOptions.hour === undefined &&
        formatOptions.minute === undefined &&
        formatOptions.second === undefined &&
        formatOptions.fractionalSecondDigits === undefined;
    }

    // Step 44.d.
    if (needDefaults && (defaults === "date" || defaults === "all")) {
      formatOptions.year = "numeric";
      formatOptions.month = "numeric";
      formatOptions.day = "numeric";
    }

    // Step 44.e.
    if (needDefaults && (defaults === "time" || defaults === "all")) {
      formatOptions.hour = "numeric";
      formatOptions.minute = "numeric";
      formatOptions.second = "numeric";
    }

    // Steps 44.f-h provided by ICU, more or less.
  }

  // Steps 45-50. (see resolveDateTimeFormatInternals).

  // We've done everything that must be done now: mark the lazy data as fully
  // computed and install it.
  initializeIntlObject(
    dateTimeFormat,
    "DateTimeFormat",
    lazyDateTimeFormatData
  );

  // 11.1.1 Intl.DateTimeFormat, step 3. (Inlined call to ChainDateTimeFormat.)
  if (
    dateTimeFormat !== thisValue &&
    callFunction(
      std_Object_isPrototypeOf,
      GetBuiltinPrototype("DateTimeFormat"),
      thisValue
    )
  ) {
    DefineDataProperty(
      thisValue,
      intlFallbackSymbol(),
      dateTimeFormat,
      ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE
    );

    return thisValue;
  }

  // Step 51.
  return dateTimeFormat;
}
/* eslint-enable complexity */

/**
 * Returns the subset of the given locale list for which this locale list has a
 * matching (possibly fallback) locale. Locales appear in the same order in the
 * returned list as in the input list.
 *
 * Spec: ECMAScript Internationalization API Specification, 12.3.2.
 */
function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) {
  var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined;

  // Step 1.
  var availableLocales = "DateTimeFormat";

  // Step 2.
  var requestedLocales = CanonicalizeLocaleList(locales);

  // Step 3.
  return SupportedLocales(availableLocales, requestedLocales, options);
}

/**
 * DateTimeFormat internal properties.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.3.3.
 */
var dateTimeFormatInternalProperties = {
  localeData: dateTimeFormatLocaleData,
  relevantExtensionKeys: ["ca", "hc", "nu"],
};

function dateTimeFormatLocaleData() {
  return {
    ca: intl_availableCalendars,
    nu: getNumberingSystems,
    hc: () => {
      return [null, "h11", "h12", "h23", "h24"];
    },
    default: {
      ca: intl_defaultCalendar,
      nu: intl_numberingSystem,
      hc: () => {
        return null;
      },
    },
  };
}

/**
 * Create function to be cached and returned by Intl.DateTimeFormat.prototype.format.
 *
 * Spec: ECMAScript Internationalization API Specification, 12.1.5.
 */
function createDateTimeFormatFormat(dtf) {
  // This function is not inlined in $Intl_DateTimeFormat_format_get to avoid
  // creating a call-object on each call to $Intl_DateTimeFormat_format_get.
  return function(date) {
    // Step 1 (implicit).

    // Step 2.
    assert(IsObject(dtf), "dateTimeFormatFormatToBind called with non-Object");
    assert(
      intl_GuardToDateTimeFormat(dtf) !== null,
      "dateTimeFormatFormatToBind called with non-DateTimeFormat"
    );

    // Steps 3-4.
    var x = date === undefined ? std_Date_now() : ToNumber(date);

    // Step 5.
    return intl_FormatDateTime(dtf, x, /* formatToParts = */ false);
  };
}

/**
 * Returns a function bound to this DateTimeFormat that returns a String value
 * representing the result of calling ToNumber(date) according to the
 * effective locale and the formatting options of this DateTimeFormat.
 *
 * Spec: ECMAScript Internationalization API Specification, 12.4.3.
 */
// Uncloned functions with `$` prefix are allocated as extended function
// to store the original name in `SetCanonicalName`.
function $Intl_DateTimeFormat_format_get() {
  // Steps 1-3.
  var thisArg = UnwrapDateTimeFormat(this);
  var dtf = thisArg;
  if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
    return callFunction(
      intl_CallDateTimeFormatMethodIfWrapped,
      thisArg,
      "$Intl_DateTimeFormat_format_get"
    );
  }

  var internals = getDateTimeFormatInternals(dtf);

  // Step 4.
  if (internals.boundFormat === undefined) {
    // Steps 4.a-c.
    internals.boundFormat = createDateTimeFormatFormat(dtf);
  }

  // Step 5.
  return internals.boundFormat;
}
SetCanonicalName($Intl_DateTimeFormat_format_get, "get format");

/**
 * Intl.DateTimeFormat.prototype.formatToParts ( date )
 *
 * Spec: ECMAScript Internationalization API Specification, 12.4.4.
 */
function Intl_DateTimeFormat_formatToParts(date) {
  // Step 1.
  var dtf = this;

  // Steps 2-3.
  if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
    return callFunction(
      intl_CallDateTimeFormatMethodIfWrapped,
      this,
      date,
      "Intl_DateTimeFormat_formatToParts"
    );
  }

  // Steps 4-5.
  var x = date === undefined ? std_Date_now() : ToNumber(date);

  // Ensure the DateTimeFormat internals are resolved.
  getDateTimeFormatInternals(dtf);

  // Step 6.
  return intl_FormatDateTime(dtf, x, /* formatToParts = */ true);
}

/**
 * Intl.DateTimeFormat.prototype.formatRange ( startDate , endDate )
 *
 * Spec: Intl.DateTimeFormat.prototype.formatRange proposal
 */
function Intl_DateTimeFormat_formatRange(startDate, endDate) {
  // Step 1.
  var dtf = this;

  // Step 2.
  if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
    return callFunction(
      intl_CallDateTimeFormatMethodIfWrapped,
      this,
      startDate,
      endDate,
      "Intl_DateTimeFormat_formatRange"
    );
  }

  // Step 3.
  if (startDate === undefined || endDate === undefined) {
    ThrowTypeError(
      JSMSG_UNDEFINED_DATE,
      startDate === undefined ? "start" : "end",
      "formatRange"
    );
  }

  // Step 4.
  var x = ToNumber(startDate);

  // Step 5.
  var y = ToNumber(endDate);

  // Ensure the DateTimeFormat internals are resolved.
  getDateTimeFormatInternals(dtf);

  // Step 6.
  return intl_FormatDateTimeRange(dtf, x, y, /* formatToParts = */ false);
}

/**
 * Intl.DateTimeFormat.prototype.formatRangeToParts ( startDate , endDate )
 *
 * Spec: Intl.DateTimeFormat.prototype.formatRange proposal
 */
function Intl_DateTimeFormat_formatRangeToParts(startDate, endDate) {
  // Step 1.
  var dtf = this;

  // Step 2.
  if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
    return callFunction(
      intl_CallDateTimeFormatMethodIfWrapped,
      this,
      startDate,
      endDate,
      "Intl_DateTimeFormat_formatRangeToParts"
    );
  }

  // Step 3.
  if (startDate === undefined || endDate === undefined) {
    ThrowTypeError(
      JSMSG_UNDEFINED_DATE,
      startDate === undefined ? "start" : "end",
      "formatRangeToParts"
    );
  }

  // Step 4.
  var x = ToNumber(startDate);

  // Step 5.
  var y = ToNumber(endDate);

  // Ensure the DateTimeFormat internals are resolved.
  getDateTimeFormatInternals(dtf);

  // Step 6.
  return intl_FormatDateTimeRange(dtf, x, y, /* formatToParts = */ true);
}

/**
 * Returns the resolved options for a DateTimeFormat object.
 *
 * Spec: ECMAScript Internationalization API Specification, 12.4.5.
 */
function Intl_DateTimeFormat_resolvedOptions() {
  // Steps 1-3.
  var thisArg = UnwrapDateTimeFormat(this);
  var dtf = thisArg;
  if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) {
    return callFunction(
      intl_CallDateTimeFormatMethodIfWrapped,
      thisArg,
      "Intl_DateTimeFormat_resolvedOptions"
    );
  }

  // Ensure the internals are resolved.
  var internals = getDateTimeFormatInternals(dtf);

  // Steps 4-5.
  var result = {
    locale: internals.locale,
    calendar: internals.calendar,
    numberingSystem: internals.numberingSystem,
    timeZone: internals.timeZone,
  };

  if (internals.pattern !== undefined) {
    // The raw pattern option is only internal to Mozilla, and not part of the
    // ECMA-402 API.
    DefineDataProperty(result, "pattern", internals.pattern);
  }

  var hasDateStyle = internals.dateStyle !== undefined;
  var hasTimeStyle = internals.timeStyle !== undefined;

  if (hasDateStyle || hasTimeStyle) {
    if (hasTimeStyle) {
      // timeStyle (unlike dateStyle) requires resolving the pattern to
      // ensure "hourCycle" and "hour12" properties are added to |result|.
      intl_resolveDateTimeFormatComponents(
        dtf,
        result,
        /* includeDateTimeFields = */ false
      );
    }
    if (hasDateStyle) {
      DefineDataProperty(result, "dateStyle", internals.dateStyle);
    }
    if (hasTimeStyle) {
      DefineDataProperty(result, "timeStyle", internals.timeStyle);
    }
  } else {
    // Components bag or a (Mozilla-only) raw pattern.
    intl_resolveDateTimeFormatComponents(
      dtf,
      result,
      /* includeDateTimeFields = */ true
    );
  }

  // Step 6.
  return result;
}