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

/* Implementation of the Intl.RelativeTimeFormat proposal. */

#include "builtin/intl/RelativeTimeFormat.h"

#include "mozilla/Assertions.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/intl/RelativeTimeFormat.h"

#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/FormatBuffer.h"
#include "builtin/intl/LanguageTag.h"
#include "gc/GCContext.h"
#include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
#include "js/Printer.h"
#include "js/PropertySpec.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
#include "vm/PlainObject.h"  // js::PlainObject
#include "vm/StringType.h"

#include "vm/NativeObject-inl.h"

using namespace js;

/**************** RelativeTimeFormat *****************/

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

const JSClass RelativeTimeFormatObject::class_ = {
    "Intl.RelativeTimeFormat",
    JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) |
        JSCLASS_FOREGROUND_FINALIZE,
    &RelativeTimeFormatObject::classOps_,
    &RelativeTimeFormatObject::classSpec_,
};

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

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

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

static const JSFunctionSpec relativeTimeFormat_methods[] = {
    JS_SELF_HOSTED_FN("resolvedOptions",
                      "Intl_RelativeTimeFormat_resolvedOptions", 0, 0),
    JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0),
    JS_SELF_HOSTED_FN("formatToParts", "Intl_RelativeTimeFormat_formatToParts",
                      2, 0),
    JS_FN("toSource", relativeTimeFormat_toSource, 0, 0),
    JS_FS_END,
};

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

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

const ClassSpec RelativeTimeFormatObject::classSpec_ = {
    GenericCreateConstructor<RelativeTimeFormat, 0, gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<RelativeTimeFormatObject>,
    relativeTimeFormat_static_methods,
    nullptr,
    relativeTimeFormat_methods,
    relativeTimeFormat_properties,
    nullptr,
    ClassSpec::DontDefineConstructor,
};

/**
 * RelativeTimeFormat constructor.
 * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1
 */
static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // Step 1.
  if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) {
    return false;
  }

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

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

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

  // Step 3.
  if (!intl::InitializeObject(cx, relativeTimeFormat,
                              cx->names().InitializeRelativeTimeFormat, locales,
                              options)) {
    return false;
  }

  args.rval().setObject(*relativeTimeFormat);
  return true;
}

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

  if (mozilla::intl::RelativeTimeFormat* rtf =
          obj->as<RelativeTimeFormatObject>().getRelativeTimeFormatter()) {
    intl::RemoveICUCellMemory(gcx, obj,
                              RelativeTimeFormatObject::EstimatedMemoryUse);

    // This was allocated using `new` in mozilla::intl::RelativeTimeFormat,
    // so we delete here.
    delete rtf;
  }
}

/**
 * Returns a new URelativeDateTimeFormatter with the locale and options of the
 * given RelativeTimeFormatObject.
 */
static mozilla::intl::RelativeTimeFormat* NewRelativeTimeFormatter(
    JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
  RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat));
  if (!internals) {
    return nullptr;
  }

  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;
  }

  UniqueChars locale = buffer.extractStringZ();
  if (!locale) {
    return nullptr;
  }

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

  using RelativeTimeFormatOptions = mozilla::intl::RelativeTimeFormatOptions;
  RelativeTimeFormatOptions options;
  {
    JSLinearString* style = value.toString()->ensureLinear(cx);
    if (!style) {
      return nullptr;
    }

    if (StringEqualsLiteral(style, "short")) {
      options.style = RelativeTimeFormatOptions::Style::Short;
    } else if (StringEqualsLiteral(style, "narrow")) {
      options.style = RelativeTimeFormatOptions::Style::Narrow;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(style, "long"));
      options.style = RelativeTimeFormatOptions::Style::Long;
    }
  }

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

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

    if (StringEqualsLiteral(numeric, "auto")) {
      options.numeric = RelativeTimeFormatOptions::Numeric::Auto;
    } else {
      MOZ_ASSERT(StringEqualsLiteral(numeric, "always"));
      options.numeric = RelativeTimeFormatOptions::Numeric::Always;
    }
  }

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

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

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

static mozilla::intl::RelativeTimeFormat* GetOrCreateRelativeTimeFormat(
    JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
  // Obtain a cached RelativeDateTimeFormatter object.
  mozilla::intl::RelativeTimeFormat* rtf =
      relativeTimeFormat->getRelativeTimeFormatter();
  if (rtf) {
    return rtf;
  }

  rtf = NewRelativeTimeFormatter(cx, relativeTimeFormat);
  if (!rtf) {
    return nullptr;
  }
  relativeTimeFormat->setRelativeTimeFormatter(rtf);

  intl::AddICUCellMemory(relativeTimeFormat,
                         RelativeTimeFormatObject::EstimatedMemoryUse);
  return rtf;
}

bool js::intl_FormatRelativeTime(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].isNumber());
  MOZ_ASSERT(args[2].isString());
  MOZ_ASSERT(args[3].isBoolean());

  Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
  relativeTimeFormat = &args[0].toObject().as<RelativeTimeFormatObject>();

  bool formatToParts = args[3].toBoolean();

  // PartitionRelativeTimePattern, step 4.
  double t = args[1].toNumber();
  if (!std::isfinite(t)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat",
                              formatToParts ? "formatToParts" : "format");
    return false;
  }

  mozilla::intl::RelativeTimeFormat* rtf =
      GetOrCreateRelativeTimeFormat(cx, relativeTimeFormat);
  if (!rtf) {
    return false;
  }

  intl::FieldType jsUnitType;
  using FormatUnit = mozilla::intl::RelativeTimeFormat::FormatUnit;
  FormatUnit relTimeUnit;
  {
    JSLinearString* unit = args[2].toString()->ensureLinear(cx);
    if (!unit) {
      return false;
    }

    // PartitionRelativeTimePattern, step 5.
    if (StringEqualsLiteral(unit, "second") ||
        StringEqualsLiteral(unit, "seconds")) {
      jsUnitType = &JSAtomState::second;
      relTimeUnit = FormatUnit::Second;
    } else if (StringEqualsLiteral(unit, "minute") ||
               StringEqualsLiteral(unit, "minutes")) {
      jsUnitType = &JSAtomState::minute;
      relTimeUnit = FormatUnit::Minute;
    } else if (StringEqualsLiteral(unit, "hour") ||
               StringEqualsLiteral(unit, "hours")) {
      jsUnitType = &JSAtomState::hour;
      relTimeUnit = FormatUnit::Hour;
    } else if (StringEqualsLiteral(unit, "day") ||
               StringEqualsLiteral(unit, "days")) {
      jsUnitType = &JSAtomState::day;
      relTimeUnit = FormatUnit::Day;
    } else if (StringEqualsLiteral(unit, "week") ||
               StringEqualsLiteral(unit, "weeks")) {
      jsUnitType = &JSAtomState::week;
      relTimeUnit = FormatUnit::Week;
    } else if (StringEqualsLiteral(unit, "month") ||
               StringEqualsLiteral(unit, "months")) {
      jsUnitType = &JSAtomState::month;
      relTimeUnit = FormatUnit::Month;
    } else if (StringEqualsLiteral(unit, "quarter") ||
               StringEqualsLiteral(unit, "quarters")) {
      jsUnitType = &JSAtomState::quarter;
      relTimeUnit = FormatUnit::Quarter;
    } else if (StringEqualsLiteral(unit, "year") ||
               StringEqualsLiteral(unit, "years")) {
      jsUnitType = &JSAtomState::year;
      relTimeUnit = FormatUnit::Year;
    } else {
      if (auto unitChars = QuoteString(cx, unit, '"')) {
        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                  JSMSG_INVALID_OPTION_VALUE, "unit",
                                  unitChars.get());
      }
      return false;
    }
  }

  using ICUError = mozilla::intl::ICUError;
  if (formatToParts) {
    mozilla::intl::NumberPartVector parts;
    mozilla::Result<mozilla::Span<const char16_t>, ICUError> result =
        rtf->formatToParts(t, relTimeUnit, parts);

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

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

    return js::intl::FormattedRelativeTimeToParts(cx, str, parts, jsUnitType,
                                                  args.rval());
  }

  js::intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
  mozilla::Result<Ok, ICUError> result = rtf->format(t, relTimeUnit, buffer);

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

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

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