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

#ifdef DEBUG
#define assertIsValidAndCanonicalLanguageTag(locale, desc) \
  do { \
    let canonical = intl_TryValidateAndCanonicalizeLanguageTag(locale); \
    assert(canonical !== null, \
           `${desc} is a structurally valid language tag`); \
    assert(canonical === locale, \
           `${desc} is a canonicalized language tag`); \
  } while (false)
#else
#define assertIsValidAndCanonicalLanguageTag(locale, desc) ; // Elided assertion.
#endif

/**
 * Returns the start index of a "Unicode locale extension sequence", which the
 * specification defines as: "any substring of a language tag that starts with
 * a separator '-' and the singleton 'u' and includes the maximum sequence of
 * following non-singleton subtags and their preceding '-' separators."
 *
 * Alternatively, this may be defined as: the components of a language tag that
 * match the `unicode_locale_extensions` production in UTS 35.
 *
 * Spec: ECMAScript Internationalization API Specification, 6.2.1.
 */
function startOfUnicodeExtensions(locale) {
  assert(typeof locale === "string", "locale is a string");

  // Search for "-u-" marking the start of a Unicode extension sequence.
  var start = callFunction(std_String_indexOf, locale, "-u-");
  if (start < 0) {
    return -1;
  }

  // And search for "-x-" marking the start of any privateuse component to
  // handle the case when "-u-" was only found within a privateuse subtag.
  var privateExt = callFunction(std_String_indexOf, locale, "-x-");
  if (privateExt >= 0 && privateExt < start) {
    return -1;
  }

  return start;
}

/**
 * Returns the end index of a Unicode locale extension sequence.
 */
function endOfUnicodeExtensions(locale, start) {
  assert(typeof locale === "string", "locale is a string");
  assert(0 <= start && start < locale.length, "start is an index into locale");
  assert(
    Substring(locale, start, 3) === "-u-",
    "start points to Unicode extension sequence"
  );

  // Search for the start of the next singleton or privateuse subtag.
  //
  // Begin searching after the smallest possible Unicode locale extension
  // sequence, namely |"-u-" 2alphanum|. End searching once the remaining
  // characters can't fit the smallest possible singleton or privateuse
  // subtag, namely |"-x-" alphanum|. Note the reduced end-limit means
  // indexing inside the loop is always in-range.
  for (var i = start + 5, end = locale.length - 4; i <= end; i++) {
    if (locale[i] !== "-") {
      continue;
    }
    if (locale[i + 2] === "-") {
      return i;
    }

    // Skip over (i + 1) and (i + 2) because we've just verified they
    // aren't "-", so the next possible delimiter can only be at (i + 3).
    i += 2;
  }

  // If no singleton or privateuse subtag was found, the Unicode extension
  // sequence extends until the end of the string.
  return locale.length;
}

/**
 * Removes Unicode locale extension sequences from the given language tag.
 */
function removeUnicodeExtensions(locale) {
  assertIsValidAndCanonicalLanguageTag(
    locale,
    "locale with possible Unicode extension"
  );

  var start = startOfUnicodeExtensions(locale);
  if (start < 0) {
    return locale;
  }

  var end = endOfUnicodeExtensions(locale, start);

  var left = Substring(locale, 0, start);
  var right = Substring(locale, end, locale.length - end);
  var combined = left + right;

  assertIsValidAndCanonicalLanguageTag(combined, "the recombined locale");
  assert(
    startOfUnicodeExtensions(combined) < 0,
    "recombination failed to remove all Unicode locale extension sequences"
  );

  return combined;
}

/**
 * Returns Unicode locale extension sequences from the given language tag.
 */
function getUnicodeExtensions(locale) {
  assertIsValidAndCanonicalLanguageTag(locale, "locale with Unicode extension");

  var start = startOfUnicodeExtensions(locale);
  assert(start >= 0, "start of Unicode extension sequence not found");
  var end = endOfUnicodeExtensions(locale, start);

  return Substring(locale, start, end - start);
}

/**
 * Returns true if the input contains only ASCII alphabetical characters.
 */
function IsASCIIAlphaString(s) {
  assert(typeof s === "string", "IsASCIIAlphaString");

  for (var i = 0; i < s.length; i++) {
    var c = callFunction(std_String_charCodeAt, s, i);
    if (!((0x41 <= c && c <= 0x5a) || (0x61 <= c && c <= 0x7a))) {
      return false;
    }
  }
  return true;
}

var localeCache = {
  runtimeDefaultLocale: undefined,
  defaultLocale: undefined,
};

/**
 * Returns the BCP 47 language tag for the host environment's current locale.
 *
 * Spec: ECMAScript Internationalization API Specification, 6.2.4.
 */
function DefaultLocale() {
  if (intl_IsRuntimeDefaultLocale(localeCache.runtimeDefaultLocale)) {
    return localeCache.defaultLocale;
  }

  // If we didn't have a cache hit, compute the candidate default locale.
  var runtimeDefaultLocale = intl_RuntimeDefaultLocale();
  var locale = intl_supportedLocaleOrFallback(runtimeDefaultLocale);

  assertIsValidAndCanonicalLanguageTag(locale, "the computed default locale");
  assert(
    startOfUnicodeExtensions(locale) < 0,
    "the computed default locale must not contain a Unicode extension sequence"
  );

  // Cache the computed locale until the runtime default locale changes.
  localeCache.defaultLocale = locale;
  localeCache.runtimeDefaultLocale = runtimeDefaultLocale;

  return locale;
}

/**
 * Canonicalizes a locale list.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.1.
 */
function CanonicalizeLocaleList(locales) {
  // Step 1.
  if (locales === undefined) {
    return [];
  }

  // Step 3 (and the remaining steps).
  var tag = intl_ValidateAndCanonicalizeLanguageTag(locales, false);
  if (tag !== null) {
    assert(
      typeof tag === "string",
      "intl_ValidateAndCanonicalizeLanguageTag returns a string value"
    );
    return [tag];
  }

  // Step 2.
  var seen = [];

  // Step 4.
  var O = ToObject(locales);

  // Step 5.
  var len = ToLength(O.length);

  // Step 6.
  var k = 0;

  // Step 7.
  while (k < len) {
    // Steps 7.a-c.
    if (k in O) {
      // Step 7.c.i.
      var kValue = O[k];

      // Step 7.c.ii.
      if (!(typeof kValue === "string" || IsObject(kValue))) {
        ThrowTypeError(JSMSG_INVALID_LOCALES_ELEMENT);
      }

      // Steps 7.c.iii-iv.
      var tag = intl_ValidateAndCanonicalizeLanguageTag(kValue, true);
      assert(
        typeof tag === "string",
        "ValidateAndCanonicalizeLanguageTag returns a string value"
      );

      // Step 7.c.v.
      if (callFunction(std_Array_indexOf, seen, tag) === -1) {
        DefineDataProperty(seen, seen.length, tag);
      }
    }

    // Step 7.d.
    k++;
  }

  // Step 8.
  return seen;
}

/**
 * Compares a BCP 47 language tag against the locales in availableLocales
 * and returns the best available match. Uses the fallback
 * mechanism of RFC 4647, section 3.4.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.2.
 * Spec: RFC 4647, section 3.4.
 */
function BestAvailableLocale(availableLocales, locale) {
  return intl_BestAvailableLocale(availableLocales, locale, DefaultLocale());
}

/**
 * Identical to BestAvailableLocale, but does not consider the default locale
 * during computation.
 */
function BestAvailableLocaleIgnoringDefault(availableLocales, locale) {
  return intl_BestAvailableLocale(availableLocales, locale, null);
}

/**
 * Compares a BCP 47 language priority list against the set of locales in
 * availableLocales and determines the best available language to meet the
 * request. Options specified through Unicode extension subsequences are
 * ignored in the lookup, but information about such subsequences is returned
 * separately.
 *
 * This variant is based on the Lookup algorithm of RFC 4647 section 3.4.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.3.
 * Spec: RFC 4647, section 3.4.
 */
function LookupMatcher(availableLocales, requestedLocales) {
  // Step 1.
  var result = new_Record();

  // Step 2.
  for (var i = 0; i < requestedLocales.length; i++) {
    var locale = requestedLocales[i];

    // Step 2.a.
    var noExtensionsLocale = removeUnicodeExtensions(locale);

    // Step 2.b.
    var availableLocale = BestAvailableLocale(
      availableLocales,
      noExtensionsLocale
    );

    // Step 2.c.
    if (availableLocale !== undefined) {
      // Step 2.c.i.
      result.locale = availableLocale;

      // Step 2.c.ii.
      if (locale !== noExtensionsLocale) {
        result.extension = getUnicodeExtensions(locale);
      }

      // Step 2.c.iii.
      return result;
    }
  }

  // Steps 3-4.
  result.locale = DefaultLocale();

  // Step 5.
  return result;
}

/**
 * Compares a BCP 47 language priority list against the set of locales in
 * availableLocales and determines the best available language to meet the
 * request. Options specified through Unicode extension subsequences are
 * ignored in the lookup, but information about such subsequences is returned
 * separately.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.4.
 */
function BestFitMatcher(availableLocales, requestedLocales) {
  // this implementation doesn't have anything better
  return LookupMatcher(availableLocales, requestedLocales);
}

/**
 * Returns the Unicode extension value subtags for the requested key subtag.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.5.
 */
function UnicodeExtensionValue(extension, key) {
  assert(typeof extension === "string", "extension is a string value");
  assert(
    callFunction(std_String_startsWith, extension, "-u-") &&
      getUnicodeExtensions("und" + extension) === extension,
    "extension is a Unicode extension subtag"
  );
  assert(typeof key === "string", "key is a string value");

  // Step 1.
  assert(key.length === 2, "key is a Unicode extension key subtag");

  // Step 2.
  var size = extension.length;

  // Step 3.
  var searchValue = "-" + key + "-";

  // Step 4.
  var pos = callFunction(std_String_indexOf, extension, searchValue);

  // Step 5.
  if (pos !== -1) {
    // Step 5.a.
    var start = pos + 4;

    // Step 5.b.
    var end = start;

    // Step 5.c.
    var k = start;

    // Steps 5.d-e.
    while (true) {
      // Step 5.e.i.
      var e = callFunction(std_String_indexOf, extension, "-", k);

      // Step 5.e.ii.
      var len = e === -1 ? size - k : e - k;

      // Step 5.e.iii.
      if (len === 2) {
        break;
      }

      // Step 5.e.iv.
      if (e === -1) {
        end = size;
        break;
      }

      // Step 5.e.v.
      end = e;
      k = e + 1;
    }

    // Step 5.f.
    return callFunction(String_substring, extension, start, end);
  }

  // Step 6.
  searchValue = "-" + key;

  // Steps 7-8.
  if (callFunction(std_String_endsWith, extension, searchValue)) {
    return "";
  }

  // Step 9 (implicit).
}

/**
 * Compares a BCP 47 language priority list against availableLocales and
 * determines the best available language to meet the request. Options specified
 * through Unicode extension subsequences are negotiated separately, taking the
 * caller's relevant extensions and locale data as well as client-provided
 * options into consideration.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.6.
 */
function ResolveLocale(
  availableLocales,
  requestedLocales,
  options,
  relevantExtensionKeys,
  localeData
) {
  // Steps 1-3.
  var matcher = options.localeMatcher;
  var r =
    matcher === "lookup"
      ? LookupMatcher(availableLocales, requestedLocales)
      : BestFitMatcher(availableLocales, requestedLocales);

  // Step 4.
  var foundLocale = r.locale;
  var extension = r.extension;

  // Step 5.
  var result = new_Record();

  // Step 6.
  result.dataLocale = foundLocale;

  // Step 7.
  var supportedExtension = "-u";

  // In this implementation, localeData is a function, not an object.
  var localeDataProvider = localeData();

  // Step 8.
  for (var i = 0; i < relevantExtensionKeys.length; i++) {
    var key = relevantExtensionKeys[i];

    // Steps 8.a-h (The locale data is only computed when needed).
    var keyLocaleData = undefined;
    var value = undefined;

    // Locale tag may override.

    // Step 8.g.
    var supportedExtensionAddition = "";

    // Step 8.h.
    if (extension !== undefined) {
      // Step 8.h.i.
      var requestedValue = UnicodeExtensionValue(extension, key);

      // Step 8.h.ii.
      if (requestedValue !== undefined) {
        // Steps 8.a-d.
        keyLocaleData = callFunction(
          localeDataProvider[key],
          null,
          foundLocale
        );

        // Step 8.h.ii.1.
        if (requestedValue !== "") {
          // Step 8.h.ii.1.a.
          if (
            callFunction(std_Array_indexOf, keyLocaleData, requestedValue) !==
            -1
          ) {
            value = requestedValue;
            supportedExtensionAddition = "-" + key + "-" + value;
          }
        } else {
          // Step 8.h.ii.2.

          // According to the LDML spec, if there's no type value,
          // and true is an allowed value, it's used.
          if (callFunction(std_Array_indexOf, keyLocaleData, "true") !== -1) {
            value = "true";
            supportedExtensionAddition = "-" + key;
          }
        }
      }
    }

    // Options override all.

    // Step 8.i.i.
    var optionsValue = options[key];

    // Step 8.i.ii.
    assert(
      typeof optionsValue === "string" ||
        optionsValue === undefined ||
        optionsValue === null,
      "unexpected type for options value"
    );

    // Steps 8.i, 8.i.iii.1.
    if (optionsValue !== undefined && optionsValue !== value) {
      // Steps 8.a-d.
      if (keyLocaleData === undefined) {
        keyLocaleData = callFunction(
          localeDataProvider[key],
          null,
          foundLocale
        );
      }

      // Step 8.i.iii.
      if (callFunction(std_Array_indexOf, keyLocaleData, optionsValue) !== -1) {
        value = optionsValue;
        supportedExtensionAddition = "";
      }
    }

    // Locale data provides default value.
    if (value === undefined) {
      // Steps 8.a-f.
      value =
        keyLocaleData === undefined
          ? callFunction(localeDataProvider.default[key], null, foundLocale)
          : keyLocaleData[0];
    }

    // Step 8.j.
    assert(
      typeof value === "string" || value === null,
      "unexpected locale data value"
    );
    result[key] = value;

    // Step 8.k.
    supportedExtension += supportedExtensionAddition;
  }

  // Step 9.
  if (supportedExtension.length > 2) {
    foundLocale = addUnicodeExtension(foundLocale, supportedExtension);
  }

  // Step 10.
  result.locale = foundLocale;

  // Step 11.
  return result;
}

/**
 * Adds a Unicode extension subtag to a locale.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.6.
 */
function addUnicodeExtension(locale, extension) {
  assert(typeof locale === "string", "locale is a string value");
  assert(
    !callFunction(std_String_startsWith, locale, "x-"),
    "unexpected privateuse-only locale"
  );
  assert(
    startOfUnicodeExtensions(locale) < 0,
    "Unicode extension subtag already present in locale"
  );

  assert(typeof extension === "string", "extension is a string value");
  assert(
    callFunction(std_String_startsWith, extension, "-u-") &&
      getUnicodeExtensions("und" + extension) === extension,
    "extension is a Unicode extension subtag"
  );

  // Step 9.a.
  var privateIndex = callFunction(std_String_indexOf, locale, "-x-");

  // Steps 9.b-c.
  if (privateIndex === -1) {
    locale += extension;
  } else {
    var preExtension = callFunction(String_substring, locale, 0, privateIndex);
    var postExtension = callFunction(String_substring, locale, privateIndex);
    locale = preExtension + extension + postExtension;
  }

  // Steps 9.d-e (Step 9.e is not required in this implementation, because we don't canonicalize
  // Unicode extension subtags).
  assertIsValidAndCanonicalLanguageTag(locale, "locale after concatenation");

  return locale;
}

/**
 * Returns the subset of requestedLocales for which availableLocales 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, 9.2.7.
 */
function LookupSupportedLocales(availableLocales, requestedLocales) {
  // Step 1.
  var subset = [];

  // Step 2.
  for (var i = 0; i < requestedLocales.length; i++) {
    var locale = requestedLocales[i];

    // Step 2.a.
    var noExtensionsLocale = removeUnicodeExtensions(locale);

    // Step 2.b.
    var availableLocale = BestAvailableLocale(
      availableLocales,
      noExtensionsLocale
    );

    // Step 2.c.
    if (availableLocale !== undefined) {
      DefineDataProperty(subset, subset.length, locale);
    }
  }

  // Step 3.
  return subset;
}

/**
 * Returns the subset of requestedLocales for which availableLocales 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, 9.2.8.
 */
function BestFitSupportedLocales(availableLocales, requestedLocales) {
  // don't have anything better
  return LookupSupportedLocales(availableLocales, requestedLocales);
}

/**
 * Returns the subset of requestedLocales for which availableLocales 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, 9.2.9.
 */
function SupportedLocales(availableLocales, requestedLocales, options) {
  // Step 1.
  var matcher;
  if (options !== undefined) {
    // Step 1.a.
    options = ToObject(options);

    // Step 1.b
    matcher = options.localeMatcher;
    if (matcher !== undefined) {
      matcher = ToString(matcher);
      if (matcher !== "lookup" && matcher !== "best fit") {
        ThrowRangeError(JSMSG_INVALID_LOCALE_MATCHER, matcher);
      }
    }
  }

  // Steps 2-5.
  return matcher === undefined || matcher === "best fit"
    ? BestFitSupportedLocales(availableLocales, requestedLocales)
    : LookupSupportedLocales(availableLocales, requestedLocales);
}

/**
 * Extracts a property value from the provided options object, converts it to
 * the required type, checks whether it is one of a list of allowed values,
 * and fills in a fallback value if necessary.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.10.
 */
function GetOption(options, property, type, values, fallback) {
  // Step 1.
  var value = options[property];

  // Step 2.
  if (value !== undefined) {
    // Steps 2.a-c.
    if (type === "boolean") {
      value = ToBoolean(value);
    } else if (type === "string") {
      value = ToString(value);
    } else {
      assert(false, "GetOption");
    }

    // Step 2.d.
    if (
      values !== undefined &&
      callFunction(std_Array_indexOf, values, value) === -1
    ) {
      ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, property, `"${value}"`);
    }

    // Step 2.e.
    return value;
  }

  // Step 3.
  return fallback;
}

/**
 * Extracts a property value from the provided options object, converts it to
 * a boolean or string, checks whether it is one of a list of allowed values,
 * and fills in a fallback value if necessary.
 */
function GetStringOrBooleanOption(
  options,
  property,
  values,
  trueValue,
  falsyValue,
  fallback
) {
  assert(IsObject(values), "GetStringOrBooleanOption");

  // Step 1.
  var value = options[property];

  // Step 2.
  if (value === undefined) {
    return fallback;
  }

  // Step 3.
  if (value === true) {
    return trueValue;
  }

  // Steps 4-5.
  if (!value) {
    return falsyValue;
  }

  // Step 6.
  value = ToString(value);

  // Step 7.
  if (callFunction(std_Array_indexOf, values, value) === -1) {
    return fallback;
  }

  // Step 8.
  return value;
}

/**
 * The abstract operation DefaultNumberOption converts value to a Number value,
 * checks whether it is in the allowed range, and fills in a fallback value if
 * necessary.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.11.
 */
function DefaultNumberOption(value, minimum, maximum, fallback) {
  assert(
    typeof minimum === "number" && (minimum | 0) === minimum,
    "DefaultNumberOption"
  );
  assert(
    typeof maximum === "number" && (maximum | 0) === maximum,
    "DefaultNumberOption"
  );
  assert(
    fallback === undefined ||
      (typeof fallback === "number" && (fallback | 0) === fallback),
    "DefaultNumberOption"
  );
  assert(
    fallback === undefined || (minimum <= fallback && fallback <= maximum),
    "DefaultNumberOption"
  );

  // Step 1.
  if (value === undefined) {
    return fallback;
  }

  // Step 2.
  value = ToNumber(value);

  // Step 3.
  if (Number_isNaN(value) || value < minimum || value > maximum) {
    ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, value);
  }

  // Step 4.
  // Apply bitwise-or to convert -0 to +0 per ES2017, 5.2 and to ensure the
  // result is an int32 value.
  return std_Math_floor(value) | 0;
}

/**
 * Extracts a property value from the provided options object, converts it to a
 * Number value, checks whether it is in the allowed range, and fills in a
 * fallback value if necessary.
 *
 * Spec: ECMAScript Internationalization API Specification, 9.2.12.
 */
function GetNumberOption(options, property, minimum, maximum, fallback) {
  // Steps 1-2.
  return DefaultNumberOption(options[property], minimum, maximum, fallback);
}

// Symbols in the self-hosting compartment can't be cloned, use a separate
// object to hold the actual symbol value.
// TODO: Can we add support to clone symbols?
var intlFallbackSymbolHolder = { value: undefined };

/**
 * The [[FallbackSymbol]] symbol of the %Intl% intrinsic object.
 *
 * This symbol is used to implement the legacy constructor semantics for
 * Intl.DateTimeFormat and Intl.NumberFormat.
 */
function intlFallbackSymbol() {
  var fallbackSymbol = intlFallbackSymbolHolder.value;
  if (!fallbackSymbol) {
    let Symbol = GetBuiltinConstructor("Symbol");
    fallbackSymbol = Symbol("IntlLegacyConstructedSymbol");
    intlFallbackSymbolHolder.value = fallbackSymbol;
  }
  return fallbackSymbol;
}

/**
 * Initializes the INTL_INTERNALS_OBJECT_SLOT of the given object.
 */
function initializeIntlObject(obj, type, lazyData) {
  assert(IsObject(obj), "Non-object passed to initializeIntlObject");
  assert(
    (type === "Collator" && intl_GuardToCollator(obj) !== null) ||
      (type === "DateTimeFormat" && intl_GuardToDateTimeFormat(obj) !== null) ||
      (type === "DisplayNames" && intl_GuardToDisplayNames(obj) !== null) ||
      (type === "ListFormat" && intl_GuardToListFormat(obj) !== null) ||
      (type === "NumberFormat" && intl_GuardToNumberFormat(obj) !== null) ||
      (type === "PluralRules" && intl_GuardToPluralRules(obj) !== null) ||
      (type === "RelativeTimeFormat" &&
        intl_GuardToRelativeTimeFormat(obj) !== null),
    "type must match the object's class"
  );
  assert(IsObject(lazyData), "non-object lazy data");

  // The meaning of an internals object for an object |obj| is as follows.
  //
  // The .type property indicates the type of Intl object that |obj| is. It
  // must be one of:
  // - Collator
  // - DateTimeFormat
  // - DisplayNames
  // - ListFormat
  // - NumberFormat
  // - PluralRules
  // - RelativeTimeFormat
  //
  // The .lazyData property stores information needed to compute -- without
  // observable side effects -- the actual internal Intl properties of
  // |obj|.  If it is non-null, then the actual internal properties haven't
  // been computed, and .lazyData must be processed by
  // |setInternalProperties| before internal Intl property values are
  // available.  If it is null, then the .internalProps property contains an
  // object whose properties are the internal Intl properties of |obj|.

  var internals = std_Object_create(null);
  internals.type = type;
  internals.lazyData = lazyData;
  internals.internalProps = null;

  assert(
    UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT) === undefined,
    "Internal slot already initialized?"
  );
  UnsafeSetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT, internals);
}

/**
 * Set the internal properties object for an |internals| object previously
 * associated with lazy data.
 */
function setInternalProperties(internals, internalProps) {
  assert(IsObject(internals.lazyData), "lazy data must exist already");
  assert(IsObject(internalProps), "internalProps argument should be an object");

  // Set in reverse order so that the .lazyData nulling is a barrier.
  internals.internalProps = internalProps;
  internals.lazyData = null;
}

/**
 * Get the existing internal properties out of a non-newborn |internals|, or
 * null if none have been computed.
 */
function maybeInternalProperties(internals) {
  assert(IsObject(internals), "non-object passed to maybeInternalProperties");
  var lazyData = internals.lazyData;
  if (lazyData) {
    return null;
  }
  assert(
    IsObject(internals.internalProps),
    "missing lazy data and computed internals"
  );
  return internals.internalProps;
}

/**
 * Return |obj|'s internals object (*not* the object holding its internal
 * properties!), with structure specified above.
 *
 * Spec: ECMAScript Internationalization API Specification, 10.3.
 * Spec: ECMAScript Internationalization API Specification, 11.3.
 * Spec: ECMAScript Internationalization API Specification, 12.3.
 */
function getIntlObjectInternals(obj) {
  assert(IsObject(obj), "getIntlObjectInternals called with non-Object");
  assert(
    intl_GuardToCollator(obj) !== null ||
      intl_GuardToDateTimeFormat(obj) !== null ||
      intl_GuardToDisplayNames(obj) !== null ||
      intl_GuardToListFormat(obj) !== null ||
      intl_GuardToNumberFormat(obj) !== null ||
      intl_GuardToPluralRules(obj) !== null ||
      intl_GuardToRelativeTimeFormat(obj) !== null,
    "getIntlObjectInternals called with non-Intl object"
  );

  var internals = UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT);

  assert(IsObject(internals), "internals not an object");
  assert(hasOwn("type", internals), "missing type");
  assert(
    (internals.type === "Collator" && intl_GuardToCollator(obj) !== null) ||
      (internals.type === "DateTimeFormat" &&
        intl_GuardToDateTimeFormat(obj) !== null) ||
      (internals.type === "DisplayNames" &&
        intl_GuardToDisplayNames(obj) !== null) ||
      (internals.type === "ListFormat" &&
        intl_GuardToListFormat(obj) !== null) ||
      (internals.type === "NumberFormat" &&
        intl_GuardToNumberFormat(obj) !== null) ||
      (internals.type === "PluralRules" &&
        intl_GuardToPluralRules(obj) !== null) ||
      (internals.type === "RelativeTimeFormat" &&
        intl_GuardToRelativeTimeFormat(obj) !== null),
    "type must match the object's class"
  );
  assert(hasOwn("lazyData", internals), "missing lazyData");
  assert(hasOwn("internalProps", internals), "missing internalProps");

  return internals;
}

/**
 * Get the internal properties of known-Intl object |obj|.  For use only by
 * C++ code that knows what it's doing!
 */
function getInternals(obj) {
  var internals = getIntlObjectInternals(obj);

  // 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.
  var type = internals.type;
  if (type === "Collator") {
    internalProps = resolveCollatorInternals(internals.lazyData);
  } else if (type === "DateTimeFormat") {
    internalProps = resolveDateTimeFormatInternals(internals.lazyData);
  } else if (type === "DisplayNames") {
    internalProps = resolveDisplayNamesInternals(internals.lazyData);
  } else if (type === "ListFormat") {
    internalProps = resolveListFormatInternals(internals.lazyData);
  } else if (type === "NumberFormat") {
    internalProps = resolveNumberFormatInternals(internals.lazyData);
  } else if (type === "PluralRules") {
    internalProps = resolvePluralRulesInternals(internals.lazyData);
  } else {
    internalProps = resolveRelativeTimeFormatInternals(internals.lazyData);
  }
  setInternalProperties(internals, internalProps);
  return internalProps;
}
