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

#include "builtin/intl/DisplayNames.h"

#include "mozilla/Assertions.h"
#include "mozilla/intl/DisplayNames.h"
#include "mozilla/PodOperations.h"
#include "mozilla/Span.h"

#include <algorithm>

#include "jsnum.h"
#include "jspubtd.h"

#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/FormatBuffer.h"
#include "gc/AllocKind.h"
#include "gc/GCContext.h"
#include "js/CallArgs.h"
#include "js/Class.h"
#include "js/experimental/Intl.h"     // JS::AddMozDisplayNamesConstructor
#include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
#include "js/Printer.h"
#include "js/PropertyAndElement.h"  // JS_DefineFunctions, JS_DefineProperties
#include "js/PropertyDescriptor.h"
#include "js/PropertySpec.h"
#include "js/RootingAPI.h"
#include "js/TypeDecls.h"
#include "js/Utility.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/Runtime.h"
#include "vm/SelfHosting.h"
#include "vm/Stack.h"
#include "vm/StringType.h"

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

using namespace js;

const JSClassOps DisplayNamesObject::classOps_ = {
    nullptr, /* addProperty */
    nullptr, /* delProperty */
    nullptr, /* enumerate */
    nullptr, /* newEnumerate */
    nullptr, /* resolve */
    nullptr, /* mayResolve */
    DisplayNamesObject::finalize,
};

const JSClass DisplayNamesObject::class_ = {
    "Intl.DisplayNames",
    JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) |
        JSCLASS_FOREGROUND_FINALIZE,
    &DisplayNamesObject::classOps_,
    &DisplayNamesObject::classSpec_,
};

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

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

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

static const JSFunctionSpec displayNames_methods[] = {
    JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0),
    JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0,
                      0),
    JS_FN("toSource", displayNames_toSource, 0, 0),
    JS_FS_END,
};

static const JSPropertySpec displayNames_properties[] = {
    JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY),
    JS_PS_END,
};

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

const ClassSpec DisplayNamesObject::classSpec_ = {
    GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<DisplayNamesObject>,
    displayNames_static_methods,
    nullptr,
    displayNames_methods,
    displayNames_properties,
    nullptr,
    ClassSpec::DontDefineConstructor,
};

enum class DisplayNamesOptions {
  Standard,

  // Calendar display names are no longer available with the current spec
  // proposal text, but may be re-enabled in the future. For our internal use
  // we still need to have them present, so use a feature guard for now.
  EnableMozExtensions,
};

/**
 * Initialize a new Intl.DisplayNames object using the named self-hosted
 * function.
 */
static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj,
                                         Handle<PropertyName*> initializer,
                                         HandleValue locales,
                                         HandleValue options,
                                         DisplayNamesOptions dnoptions) {
  FixedInvokeArgs<4> args(cx);

  args[0].setObject(*obj);
  args[1].set(locales);
  args[2].set(options);
  args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions);

  RootedValue ignored(cx);
  if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args,
                              &ignored)) {
    return false;
  }

  MOZ_ASSERT(ignored.isUndefined(),
             "Unexpected return value from non-legacy Intl object initializer");
  return true;
}

/**
 * Intl.DisplayNames ([ locales [ , options ]])
 */
static bool DisplayNames(JSContext* cx, const CallArgs& args,
                         DisplayNamesOptions dnoptions) {
  // Step 1.
  if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) {
    return false;
  }

  // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
  RootedObject proto(cx);
  if (dnoptions == DisplayNamesOptions::Standard) {
    if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames,
                                            &proto)) {
      return false;
    }
  } else {
    RootedObject newTarget(cx, &args.newTarget().toObject());
    if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) {
      return false;
    }
  }

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

  HandleValue locales = args.get(0);
  HandleValue options = args.get(1);

  // Steps 3-26.
  if (!InitializeDisplayNamesObject(cx, displayNames,
                                    cx->names().InitializeDisplayNames, locales,
                                    options, dnoptions)) {
    return false;
  }

  // Step 27.
  args.rval().setObject(*displayNames);
  return true;
}

static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return DisplayNames(cx, args, DisplayNamesOptions::Standard);
}

static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions);
}

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

  if (mozilla::intl::DisplayNames* displayNames =
          obj->as<DisplayNamesObject>().getDisplayNames()) {
    intl::RemoveICUCellMemory(gcx, obj, DisplayNamesObject::EstimatedMemoryUse);
    delete displayNames;
  }
}

bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) {
  RootedObject ctor(cx, GlobalObject::createConstructor(
                            cx, MozDisplayNames, cx->names().DisplayNames, 2));
  if (!ctor) {
    return false;
  }

  RootedObject proto(
      cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
  if (!proto) {
    return false;
  }

  if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
    return false;
  }

  if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) {
    return false;
  }

  if (!JS_DefineFunctions(cx, proto, displayNames_methods)) {
    return false;
  }

  if (!JS_DefineProperties(cx, proto, displayNames_properties)) {
    return false;
  }

  RootedValue ctorValue(cx, ObjectValue(*ctor));
  return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0);
}

static mozilla::intl::DisplayNames* NewDisplayNames(
    JSContext* cx, const char* locale,
    mozilla::intl::DisplayNames::Options& options) {
  auto result = mozilla::intl::DisplayNames::TryCreate(locale, options);
  if (result.isErr()) {
    intl::ReportInternalError(cx, result.unwrapErr());
    return nullptr;
  }
  return result.unwrap().release();
}

static mozilla::intl::DisplayNames* GetOrCreateDisplayNames(
    JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
    mozilla::intl::DisplayNames::Options& options) {
  // Obtain a cached mozilla::intl::DisplayNames object.
  mozilla::intl::DisplayNames* dn = displayNames->getDisplayNames();
  if (!dn) {
    dn = NewDisplayNames(cx, locale, options);
    if (!dn) {
      return nullptr;
    }
    displayNames->setDisplayNames(dn);

    intl::AddICUCellMemory(displayNames,
                           DisplayNamesObject::EstimatedMemoryUse);
  }
  return dn;
}

static void ReportInvalidOptionError(JSContext* cx, HandleString type,
                                     HandleString option) {
  if (UniqueChars optionStr = QuoteString(cx, option, '"')) {
    if (UniqueChars typeStr = QuoteString(cx, type)) {
      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                JSMSG_INVALID_OPTION_VALUE, typeStr.get(),
                                optionStr.get());
    }
  }
}

static void ReportInvalidOptionError(JSContext* cx, const char* type,
                                     HandleString option) {
  if (UniqueChars str = QuoteString(cx, option, '"')) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_INVALID_OPTION_VALUE, type, str.get());
  }
}

static void ReportInvalidOptionError(JSContext* cx, const char* type,
                                     double option) {
  ToCStringBuf cbuf;
  const char* str = NumberToCString(&cbuf, option);
  MOZ_ASSERT(str);
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                            JSMSG_INVALID_DIGITS_VALUE, str);
}

/**
 * intl_ComputeDisplayName(displayNames, locale, calendar, style,
 *                         languageDisplay, fallback, type, code)
 */
bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  MOZ_ASSERT(args.length() == 8);

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

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

  Rooted<JSLinearString*> calendar(cx, args[2].toString()->ensureLinear(cx));
  if (!calendar) {
    return false;
  }

  Rooted<JSLinearString*> code(cx, args[7].toString()->ensureLinear(cx));
  if (!code) {
    return false;
  }

  mozilla::intl::DisplayNames::Style style;
  {
    JSLinearString* styleStr = args[3].toString()->ensureLinear(cx);
    if (!styleStr) {
      return false;
    }

    if (StringEqualsLiteral(styleStr, "long")) {
      style = mozilla::intl::DisplayNames::Style::Long;
    } else if (StringEqualsLiteral(styleStr, "short")) {
      style = mozilla::intl::DisplayNames::Style::Short;
    } else if (StringEqualsLiteral(styleStr, "narrow")) {
      style = mozilla::intl::DisplayNames::Style::Narrow;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(styleStr, "abbreviated"));
      style = mozilla::intl::DisplayNames::Style::Abbreviated;
    }
  }

  mozilla::intl::DisplayNames::LanguageDisplay languageDisplay;
  {
    JSLinearString* language = args[4].toString()->ensureLinear(cx);
    if (!language) {
      return false;
    }

    if (StringEqualsLiteral(language, "dialect")) {
      languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Dialect;
    } else {
      MOZ_ASSERT(language->empty() ||
                 StringEqualsLiteral(language, "standard"));
      languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Standard;
    }
  }

  mozilla::intl::DisplayNames::Fallback fallback;
  {
    JSLinearString* fallbackStr = args[5].toString()->ensureLinear(cx);
    if (!fallbackStr) {
      return false;
    }

    if (StringEqualsLiteral(fallbackStr, "none")) {
      fallback = mozilla::intl::DisplayNames::Fallback::None;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(fallbackStr, "code"));
      fallback = mozilla::intl::DisplayNames::Fallback::Code;
    }
  }

  Rooted<JSLinearString*> type(cx, args[6].toString()->ensureLinear(cx));
  if (!type) {
    return false;
  }

  mozilla::intl::DisplayNames::Options options{
      style,
      languageDisplay,
  };

  // If a calendar exists, set it as an option.
  JS::UniqueChars calendarChars = nullptr;
  if (!calendar->empty()) {
    calendarChars = JS_EncodeStringToUTF8(cx, calendar);
    if (!calendarChars) {
      return false;
    }
  }

  mozilla::intl::DisplayNames* dn =
      GetOrCreateDisplayNames(cx, displayNames, locale.get(), options);
  if (!dn) {
    return false;
  }

  // The "code" is usually a small ASCII string, so try to avoid an allocation
  // by copying it to the stack. Unfortunately we can't pass a string span of
  // the JSString directly to the unified DisplayNames API, as the
  // intl::FormatBuffer will be written to. This writing can trigger a GC and
  // invalidate the span, creating a nogc rooting hazard.
  JS::UniqueChars utf8 = nullptr;
  unsigned char ascii[32];
  mozilla::Span<const char> codeSpan = nullptr;
  if (code->length() < 32 && code->hasLatin1Chars() && StringIsAscii(code)) {
    JS::AutoCheckCannotGC nogc;
    mozilla::PodCopy(ascii, code->latin1Chars(nogc), code->length());
    codeSpan =
        mozilla::Span(reinterpret_cast<const char*>(ascii), code->length());
  } else {
    utf8 = JS_EncodeStringToUTF8(cx, code);
    if (!utf8) {
      return false;
    }
    codeSpan = mozilla::MakeStringSpan(utf8.get());
  }

  intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
  mozilla::Result<mozilla::Ok, mozilla::intl::DisplayNamesError> result =
      mozilla::Ok{};

  if (StringEqualsLiteral(type, "language")) {
    result = dn->GetLanguage(buffer, codeSpan, fallback);
  } else if (StringEqualsLiteral(type, "script")) {
    result = dn->GetScript(buffer, codeSpan, fallback);
  } else if (StringEqualsLiteral(type, "region")) {
    result = dn->GetRegion(buffer, codeSpan, fallback);
  } else if (StringEqualsLiteral(type, "currency")) {
    result = dn->GetCurrency(buffer, codeSpan, fallback);
  } else if (StringEqualsLiteral(type, "calendar")) {
    result = dn->GetCalendar(buffer, codeSpan, fallback);
  } else if (StringEqualsLiteral(type, "weekday")) {
    double d = LinearStringToNumber(code);
    if (!IsInteger(d) || d < 1 || d > 7) {
      ReportInvalidOptionError(cx, "weekday", d);
      return false;
    }
    result =
        dn->GetWeekday(buffer, static_cast<mozilla::intl::Weekday>(d),
                       mozilla::MakeStringSpan(calendarChars.get()), fallback);
  } else if (StringEqualsLiteral(type, "month")) {
    double d = LinearStringToNumber(code);
    if (!IsInteger(d) || d < 1 || d > 13) {
      ReportInvalidOptionError(cx, "month", d);
      return false;
    }

    result =
        dn->GetMonth(buffer, static_cast<mozilla::intl::Month>(d),
                     mozilla::MakeStringSpan(calendarChars.get()), fallback);

  } else if (StringEqualsLiteral(type, "quarter")) {
    double d = LinearStringToNumber(code);

    // Inlined implementation of `IsValidQuarterCode ( quarter )`.
    if (!IsInteger(d) || d < 1 || d > 4) {
      ReportInvalidOptionError(cx, "quarter", d);
      return false;
    }

    result =
        dn->GetQuarter(buffer, static_cast<mozilla::intl::Quarter>(d),
                       mozilla::MakeStringSpan(calendarChars.get()), fallback);

  } else if (StringEqualsLiteral(type, "dayPeriod")) {
    mozilla::intl::DayPeriod dayPeriod;
    if (StringEqualsLiteral(code, "am")) {
      dayPeriod = mozilla::intl::DayPeriod::AM;
    } else if (StringEqualsLiteral(code, "pm")) {
      dayPeriod = mozilla::intl::DayPeriod::PM;
    } else {
      ReportInvalidOptionError(cx, "dayPeriod", code);
      return false;
    }
    result = dn->GetDayPeriod(buffer, dayPeriod,
                              mozilla::MakeStringSpan(calendarChars.get()),
                              fallback);

  } else {
    MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField"));
    mozilla::intl::DateTimeField field;
    if (StringEqualsLiteral(code, "era")) {
      field = mozilla::intl::DateTimeField::Era;
    } else if (StringEqualsLiteral(code, "year")) {
      field = mozilla::intl::DateTimeField::Year;
    } else if (StringEqualsLiteral(code, "quarter")) {
      field = mozilla::intl::DateTimeField::Quarter;
    } else if (StringEqualsLiteral(code, "month")) {
      field = mozilla::intl::DateTimeField::Month;
    } else if (StringEqualsLiteral(code, "weekOfYear")) {
      field = mozilla::intl::DateTimeField::WeekOfYear;
    } else if (StringEqualsLiteral(code, "weekday")) {
      field = mozilla::intl::DateTimeField::Weekday;
    } else if (StringEqualsLiteral(code, "day")) {
      field = mozilla::intl::DateTimeField::Day;
    } else if (StringEqualsLiteral(code, "dayPeriod")) {
      field = mozilla::intl::DateTimeField::DayPeriod;
    } else if (StringEqualsLiteral(code, "hour")) {
      field = mozilla::intl::DateTimeField::Hour;
    } else if (StringEqualsLiteral(code, "minute")) {
      field = mozilla::intl::DateTimeField::Minute;
    } else if (StringEqualsLiteral(code, "second")) {
      field = mozilla::intl::DateTimeField::Second;
    } else if (StringEqualsLiteral(code, "timeZoneName")) {
      field = mozilla::intl::DateTimeField::TimeZoneName;
    } else {
      ReportInvalidOptionError(cx, "dateTimeField", code);
      return false;
    }

    intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
    mozilla::intl::DateTimePatternGenerator* dtpgen =
        sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
    if (!dtpgen) {
      return false;
    }

    result = dn->GetDateTimeField(buffer, field, *dtpgen, fallback);
  }

  if (result.isErr()) {
    switch (result.unwrapErr()) {
      case mozilla::intl::DisplayNamesError::InternalError:
        intl::ReportInternalError(cx);
        break;
      case mozilla::intl::DisplayNamesError::OutOfMemory:
        ReportOutOfMemory(cx);
        break;
      case mozilla::intl::DisplayNamesError::InvalidOption:
        ReportInvalidOptionError(cx, type, code);
        break;
      case mozilla::intl::DisplayNamesError::DuplicateVariantSubtag:
        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                  JSMSG_DUPLICATE_VARIANT_SUBTAG);
        break;
      case mozilla::intl::DisplayNamesError::InvalidLanguageTag:
        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                  JSMSG_INVALID_LANGUAGE_TAG);
        break;
    }
    return false;
  }

  JSString* str = buffer.toString(cx);
  if (!str) {
    return false;
  }

  if (str->empty()) {
    args.rval().setUndefined();
  } else {
    args.rval().setString(str);
  }

  return true;
}
