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

#include "builtin/temporal/PlainYearMonth.h"

#include "mozilla/Assertions.h"

#include <cstdlib>
#include <initializer_list>
#include <stddef.h>
#include <type_traits>
#include <utility>

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

#include "builtin/temporal/Calendar.h"
#include "builtin/temporal/Duration.h"
#include "builtin/temporal/PlainDate.h"
#include "builtin/temporal/Temporal.h"
#include "builtin/temporal/TemporalFields.h"
#include "builtin/temporal/TemporalParser.h"
#include "builtin/temporal/TemporalRoundingMode.h"
#include "builtin/temporal/TemporalTypes.h"
#include "builtin/temporal/TemporalUnit.h"
#include "builtin/temporal/Wrapped.h"
#include "ds/IdValuePair.h"
#include "gc/AllocKind.h"
#include "gc/Barrier.h"
#include "js/AllocPolicy.h"
#include "js/CallArgs.h"
#include "js/CallNonGenericMethod.h"
#include "js/Class.h"
#include "js/Conversions.h"
#include "js/ErrorReport.h"
#include "js/friend/ErrorMessages.h"
#include "js/GCVector.h"
#include "js/Id.h"
#include "js/PropertyDescriptor.h"
#include "js/PropertySpec.h"
#include "js/RootingAPI.h"
#include "js/TypeDecls.h"
#include "js/Value.h"
#include "util/StringBuffer.h"
#include "vm/Compartment.h"
#include "vm/GlobalObject.h"
#include "vm/JSAtomState.h"
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/ObjectOperations.h"
#include "vm/PlainObject.h"
#include "vm/StringType.h"

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

using namespace js;
using namespace js::temporal;

static inline bool IsPlainYearMonth(Handle<Value> v) {
  return v.isObject() && v.toObject().is<PlainYearMonthObject>();
}

/**
 * ISOYearMonthWithinLimits ( year, month )
 */
template <typename T>
static bool ISOYearMonthWithinLimits(T year, int32_t month) {
  static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>);

  // Step 1.
  MOZ_ASSERT(IsInteger(year));
  MOZ_ASSERT(1 <= month && month <= 12);

  // Step 2.
  if (year < -271821 || year > 275760) {
    return false;
  }

  // Step 3.
  if (year == -271821 && month < 4) {
    return false;
  }

  // Step 4.
  if (year == 275760 && month > 9) {
    return false;
  }

  // Step 5.
  return true;
}

/**
 * CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ ,
 * newTarget ] )
 */
static PlainYearMonthObject* CreateTemporalYearMonth(
    JSContext* cx, const CallArgs& args, double isoYear, double isoMonth,
    double isoDay, Handle<JSObject*> calendar) {
  MOZ_ASSERT(IsInteger(isoYear));
  MOZ_ASSERT(IsInteger(isoMonth));
  MOZ_ASSERT(IsInteger(isoDay));

  // Step 1.
  if (!ThrowIfInvalidISODate(cx, isoYear, isoMonth, isoDay)) {
    return nullptr;
  }

  // FIXME: spec issue - Consider calling ISODateTimeWithinLimits to include
  // testing |referenceISODay|?

  // Step 2.
  if (!ISOYearMonthWithinLimits(isoYear, isoMonth)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID);
    return nullptr;
  }

  // Steps 3-4.
  Rooted<JSObject*> proto(cx);
  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainYearMonth,
                                          &proto)) {
    return nullptr;
  }

  auto* obj = NewObjectWithClassProto<PlainYearMonthObject>(cx, proto);
  if (!obj) {
    return nullptr;
  }

  // Step 5.
  obj->setFixedSlot(PlainYearMonthObject::ISO_YEAR_SLOT, Int32Value(isoYear));

  // Step 6.
  obj->setFixedSlot(PlainYearMonthObject::ISO_MONTH_SLOT, Int32Value(isoMonth));

  // Step 7.
  obj->setFixedSlot(PlainYearMonthObject::CALENDAR_SLOT,
                    ObjectValue(*calendar));

  // Step 8.
  obj->setFixedSlot(PlainYearMonthObject::ISO_DAY_SLOT, Int32Value(isoDay));

  // Step 9.
  return obj;
}

/**
 * CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ ,
 * newTarget ] )
 */
PlainYearMonthObject* js::temporal::CreateTemporalYearMonth(
    JSContext* cx, const PlainDate& date, Handle<JSObject*> calendar) {
  auto& [isoYear, isoMonth, isoDay] = date;

  // Step 1.
  if (!ThrowIfInvalidISODate(cx, date)) {
    return nullptr;
  }

  // FIXME: spec issue - Consider calling ISODateTimeWithinLimits to include
  // testing |referenceISODay|?

  // Step 2.
  if (!ISOYearMonthWithinLimits(isoYear, isoMonth)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID);
    return nullptr;
  }

  // Steps 3-4.
  auto* obj = NewBuiltinClassInstance<PlainYearMonthObject>(cx);
  if (!obj) {
    return nullptr;
  }

  // Step 5.
  obj->setFixedSlot(PlainYearMonthObject::ISO_YEAR_SLOT, Int32Value(isoYear));

  // Step 6.
  obj->setFixedSlot(PlainYearMonthObject::ISO_MONTH_SLOT, Int32Value(isoMonth));

  // Step 7.
  obj->setFixedSlot(PlainYearMonthObject::CALENDAR_SLOT,
                    ObjectValue(*calendar));

  // Step 8.
  obj->setFixedSlot(PlainYearMonthObject::ISO_DAY_SLOT, Int32Value(isoDay));

  // Step 9.
  return obj;
}

/**
 * ToTemporalYearMonth ( item [ , options ] )
 */
static Wrapped<PlainYearMonthObject*> ToTemporalYearMonth(
    JSContext* cx, Handle<Value> item,
    Handle<JSObject*> maybeOptions = nullptr) {
  // Steps 1-2. (Not applicable in our implementation.)

  // Step 3.
  if (item.isObject()) {
    Rooted<JSObject*> itemObj(cx, &item.toObject());

    // Step 3.a.
    if (itemObj->canUnwrapAs<PlainYearMonthObject>()) {
      return itemObj;
    }

    // Step 3.b.
    Rooted<JSObject*> calendar(cx,
                               GetTemporalCalendarWithISODefault(cx, itemObj));
    if (!calendar) {
      return nullptr;
    }

    // Step 3.c.
    JS::RootedVector<PropertyKey> fieldNames(cx);
    if (!CalendarFields(cx, calendar,
                        {CalendarField::Month, CalendarField::MonthCode,
                         CalendarField::Year},
                        &fieldNames)) {
      return nullptr;
    }

    // Step 3.d.
    Rooted<PlainObject*> fields(cx,
                                PrepareTemporalFields(cx, itemObj, fieldNames));
    if (!fields) {
      return nullptr;
    }

    // Step 3.e.
    if (maybeOptions) {
      return CalendarYearMonthFromFields(cx, calendar, fields, maybeOptions);
    }
    return CalendarYearMonthFromFields(cx, calendar, fields);
  }

  // Step 4.
  if (maybeOptions) {
    TemporalOverflow ignored;
    if (!ToTemporalOverflow(cx, maybeOptions, &ignored)) {
      return nullptr;
    }
  }

  // Step 5.
  Rooted<JSString*> string(cx, JS::ToString(cx, item));
  if (!string) {
    return nullptr;
  }

  // Step 6.
  PlainDate result;
  Rooted<JSString*> calendarString(cx);
  if (!ParseTemporalYearMonthString(cx, string, &result, &calendarString)) {
    return nullptr;
  }

  // Step 7.
  Rooted<Value> calendarLike(cx);
  if (calendarString) {
    calendarLike.setString(calendarString);
  }

  Rooted<JSObject*> calendar(
      cx, ToTemporalCalendarWithISODefault(cx, calendarLike));
  if (!calendar) {
    return nullptr;
  }

  // Step 8.
  Rooted<PlainYearMonthObject*> obj(
      cx, CreateTemporalYearMonth(cx, result, calendar));
  if (!obj) {
    return nullptr;
  }

  // Steps 9-10.
  return CalendarYearMonthFromFields(cx, calendar, obj);
}

/**
 * ToTemporalYearMonth ( item [ , options ] )
 */
static bool ToTemporalYearMonth(JSContext* cx, Handle<Value> item,
                                PlainDate* result) {
  auto obj = ToTemporalYearMonth(cx, item);
  if (!obj) {
    return false;
  }

  *result = ToPlainDate(&obj.unwrap());
  return true;
}

/**
 * ToTemporalYearMonth ( item [ , options ] )
 */
static bool ToTemporalYearMonth(JSContext* cx, Handle<Value> item,
                                PlainDate* result,
                                MutableHandle<JSObject*> calendar) {
  auto* obj = ToTemporalYearMonth(cx, item).unwrapOrNull();
  if (!obj) {
    return false;
  }

  *result = ToPlainDate(obj);
  calendar.set(obj->calendar());
  return cx->compartment()->wrap(cx, calendar);
}

/**
 * TemporalYearMonthToString ( yearMonth, showCalendar )
 */
static JSString* TemporalYearMonthToString(
    JSContext* cx, Handle<PlainYearMonthObject*> yearMonth,
    CalendarOption showCalendar) {
  // Steps 1-2. (Not applicable in our implementation.)

  // Note: This doesn't reserve too much space, because the string builder
  // already internally reserves space for 64 characters.
  constexpr size_t datePart = 1 + 6 + 1 + 2 + 1 + 2;  // 13
  constexpr size_t calendarPart = 30;

  JSStringBuilder result(cx);

  if (!result.reserve(datePart + calendarPart)) {
    return nullptr;
  }

  // Step 6. (Reordered)
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());
  JSString* str = CalendarToString(cx, calendar);
  if (!str) {
    return nullptr;
  }

  Rooted<JSLinearString*> calendarID(cx, str->ensureLinear(cx));
  if (!calendarID) {
    return nullptr;
  }

  // Step 3. (Reordered)
  int32_t year = yearMonth->isoYear();
  if (0 <= year && year <= 9999) {
    result.infallibleAppend(char('0' + (year / 1000)));
    result.infallibleAppend(char('0' + (year % 1000) / 100));
    result.infallibleAppend(char('0' + (year % 100) / 10));
    result.infallibleAppend(char('0' + (year % 10)));
  } else {
    result.infallibleAppend(year < 0 ? '-' : '+');

    year = std::abs(year);
    result.infallibleAppend(char('0' + (year / 100000)));
    result.infallibleAppend(char('0' + (year % 100000) / 10000));
    result.infallibleAppend(char('0' + (year % 10000) / 1000));
    result.infallibleAppend(char('0' + (year % 1000) / 100));
    result.infallibleAppend(char('0' + (year % 100) / 10));
    result.infallibleAppend(char('0' + (year % 10)));
  }

  // Steps 4-5.
  int32_t month = yearMonth->isoMonth();
  result.infallibleAppend('-');
  result.infallibleAppend(char('0' + (month / 10)));
  result.infallibleAppend(char('0' + (month % 10)));

  // Step 7.
  if (showCalendar == CalendarOption::Always ||
      showCalendar == CalendarOption::Critical ||
      !StringEqualsLiteral(calendarID, "iso8601")) {
    int32_t day = yearMonth->isoDay();
    result.infallibleAppend('-');
    result.infallibleAppend(char('0' + (day / 10)));
    result.infallibleAppend(char('0' + (day % 10)));
  }

  // Steps 8-9.
  if (!FormatCalendarAnnotation(cx, result, calendarID, showCalendar)) {
    return nullptr;
  }

  // Step 10.
  return result.finishString();
}

/**
 * DifferenceTemporalPlainYearMonth ( operation, yearMonth, other, options )
 */
static bool DifferenceTemporalPlainYearMonth(JSContext* cx,
                                             TemporalDifference operation,
                                             const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 1. (Not applicable in our implementation.)

  // Step 2.
  auto otherYearMonth = ToTemporalYearMonth(cx, args.get(0));
  if (!otherYearMonth) {
    return false;
  }

  Rooted<Wrapped<PlainYearMonthObject*>> other(cx, otherYearMonth);
  Rooted<JSObject*> otherCalendar(cx, otherYearMonth.unwrap().calendar());
  if (!cx->compartment()->wrap(cx, &otherCalendar)) {
    return false;
  }

  // Step 3.
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  if (!CalendarEqualsOrThrow(cx, calendar, otherCalendar)) {
    return false;
  }

  // Steps 5-7.
  Rooted<PlainObject*> resolvedOptions(cx);
  DifferenceSettings settings;
  if (args.hasDefined(1)) {
    Rooted<JSObject*> options(
        cx, RequireObjectArg(cx, "options", ToName(operation), args[1]));
    if (!options) {
      return false;
    }

    // Step 5.
    resolvedOptions = NewPlainObjectWithProto(cx, nullptr);
    if (!resolvedOptions) {
      return false;
    }

    // Step 6.
    if (!CopyDataProperties(cx, resolvedOptions, options)) {
      return false;
    }

    // Step 7.
    if (!GetDifferenceSettings(cx, operation, resolvedOptions,
                               TemporalUnitGroup::Date, TemporalUnit::Month,
                               TemporalUnit::Month, TemporalUnit::Year,
                               &settings)) {
      return false;
    }

    // Step 8.
    Rooted<Value> largestUnitValue(
        cx, StringValue(TemporalUnitToString(cx, settings.largestUnit)));
    if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit,
                            largestUnitValue)) {
      return false;
    }
  } else {
    // Steps 5-7.
    settings = {
        TemporalUnit::Month,
        TemporalUnit::Year,
        TemporalRoundingMode::Trunc,
        Increment{1},
    };

    // Step 8. (Not applicable in our implementation.)
  }

  // Step 9.
  JS::RootedVector<PropertyKey> fieldNames(cx);
  if (!CalendarFields(cx, calendar,
                      {CalendarField::MonthCode, CalendarField::Year},
                      &fieldNames)) {
    return false;
  }

  // Step 10.
  Rooted<PlainObject*> thisFields(
      cx, PrepareTemporalFields(cx, yearMonth, fieldNames));
  if (!thisFields) {
    return false;
  }

  // Step 11.
  Value one = Int32Value(1);
  auto handleOne = Handle<Value>::fromMarkedLocation(&one);
  if (!DefineDataProperty(cx, thisFields, cx->names().day, handleOne)) {
    return false;
  }

  // Step 12.
  Rooted<Wrapped<PlainDateObject*>> thisDate(
      cx, CalendarDateFromFields(cx, calendar, thisFields));
  if (!thisDate) {
    return false;
  }

  // Step 13.
  Rooted<PlainObject*> otherFields(
      cx, PrepareTemporalFields(cx, other, fieldNames));
  if (!otherFields) {
    return false;
  }

  // Step 14.
  if (!DefineDataProperty(cx, otherFields, cx->names().day, handleOne)) {
    return false;
  }

  // Step 15.
  Rooted<Wrapped<PlainDateObject*>> otherDate(
      cx, CalendarDateFromFields(cx, calendar, otherFields));
  if (!otherDate) {
    return false;
  }

  // Step 16.
  Duration result;
  if (resolvedOptions) {
    if (!CalendarDateUntil(cx, calendar, thisDate, otherDate, resolvedOptions,
                           &result)) {
      return false;
    }
  } else {
    if (!CalendarDateUntil(cx, calendar, thisDate, otherDate,
                           settings.largestUnit, &result)) {
      return false;
    }
  }

  // We only care about years and months here, all other fields are set to zero.
  Duration duration = {result.years, result.months};

  // Step 17.
  if (settings.smallestUnit != TemporalUnit::Month ||
      settings.roundingIncrement != Increment{1}) {
    Duration rounded;
    if (!RoundDuration(cx, duration, settings.roundingIncrement,
                       settings.smallestUnit, settings.roundingMode, thisDate,
                       &rounded)) {
      return false;
    }

    duration = {rounded.years, rounded.months};
  }

  // Step 18.
  if (operation == TemporalDifference::Since) {
    duration = duration.negate();
  }

  auto* obj = CreateTemporalDuration(cx, {duration.years, duration.months});
  if (!obj) {
    return false;
  }

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

enum class PlainYearMonthDuration { Add, Subtract };

/**
 * AddDurationToOrSubtractDurationFromPlainYearMonth ( operation, yearMonth,
 * temporalDurationLike, options )
 */
static bool AddDurationToOrSubtractDurationFromPlainYearMonth(
    JSContext* cx, PlainYearMonthDuration operation, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 1.
  Duration duration;
  if (!ToTemporalDurationRecord(cx, args.get(0), &duration)) {
    return false;
  }

  // Step 2.
  if (operation == PlainYearMonthDuration::Subtract) {
    duration = duration.negate();
  }

  // Step 3.
  TimeDuration balanceResult;
  if (!BalanceDuration(cx, duration, TemporalUnit::Day, &balanceResult)) {
    return false;
  }

  // Step 4.
  Rooted<JSObject*> options(cx);
  if (args.hasDefined(1)) {
    const char* name =
        operation == PlainYearMonthDuration::Add ? "add" : "subtract";
    options = RequireObjectArg(cx, "options", name, args[1]);
  } else {
    options = NewPlainObjectWithProto(cx, nullptr);
  }
  if (!options) {
    return false;
  }

  // Step 5.
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 6.
  JS::RootedVector<PropertyKey> fieldNames(cx);
  if (!CalendarFields(cx, calendar,
                      {CalendarField::MonthCode, CalendarField::Year},
                      &fieldNames)) {
    return false;
  }

  // Step 7.
  Rooted<PlainObject*> fields(cx,
                              PrepareTemporalFields(cx, yearMonth, fieldNames));
  if (!fields) {
    return false;
  }

  // Step 8.
  int32_t sign = DurationSign(
      {duration.years, duration.months, duration.weeks, balanceResult.days});

  // Steps 9-10.
  Rooted<Value> day(cx);
  if (sign < 0) {
    // Step 9.
    Rooted<Value> yearMonthValue(cx, ObjectValue(*yearMonth));
    if (!CalendarDaysInMonth(cx, calendar, yearMonthValue, &day)) {
      return false;
    }
  } else {
    // Step 10.
    day.setInt32(1);
  }

  // Step 11.
  if (!DefineDataProperty(cx, fields, cx->names().day, day)) {
    return false;
  }

  // Step 12.
  Rooted<Wrapped<PlainDateObject*>> date(
      cx, CalendarDateFromFields(cx, calendar, fields));
  if (!date) {
    return false;
  }

  // Step 13.
  Rooted<DurationObject*> durationToAdd(
      cx, CreateTemporalDuration(cx, {duration.years, duration.months,
                                      duration.weeks, balanceResult.days}));
  if (!durationToAdd) {
    return false;
  }

  // Step 14.
  Rooted<PlainObject*> optionsCopy(cx, NewPlainObjectWithProto(cx, nullptr));
  if (!optionsCopy) {
    return false;
  }

  // Step 15.
  if (!CopyDataProperties(cx, optionsCopy, options)) {
    return false;
  }

  // Step 16.
  Rooted<Wrapped<PlainDateObject*>> addedDate(
      cx, CalendarDateAdd(cx, calendar, date, durationToAdd, options));
  if (!addedDate) {
    return false;
  }

  // Step 17.
  Rooted<PlainObject*> addedDateFields(
      cx, PrepareTemporalFields(cx, addedDate, fieldNames));
  if (!addedDateFields) {
    return false;
  }

  // Step 18.
  auto obj =
      CalendarYearMonthFromFields(cx, calendar, addedDateFields, optionsCopy);
  if (!obj) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth ( isoYear, isoMonth [ , calendarLike [ ,
 * referenceISODay ] ] )
 */
static bool PlainYearMonthConstructor(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

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

  // Step 3.
  double isoYear;
  if (!ToIntegerWithTruncation(cx, args.get(0), "year", &isoYear)) {
    return false;
  }

  // Step 4.
  double isoMonth;
  if (!ToIntegerWithTruncation(cx, args.get(1), "month", &isoMonth)) {
    return false;
  }

  // Step 5.
  Rooted<JSObject*> calendar(cx,
                             ToTemporalCalendarWithISODefault(cx, args.get(2)));
  if (!calendar) {
    return false;
  }

  // Steps 2 and 6.
  double isoDay = 1;
  if (args.hasDefined(3)) {
    if (!ToIntegerWithTruncation(cx, args[3], "day", &isoDay)) {
      return false;
    }
  }

  // Step 7.
  auto* yearMonth =
      CreateTemporalYearMonth(cx, args, isoYear, isoMonth, isoDay, calendar);
  if (!yearMonth) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.from ( item [ , options ] )
 */
static bool PlainYearMonth_from(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // Step 1.
  Rooted<JSObject*> options(cx);
  if (args.hasDefined(1)) {
    options = RequireObjectArg(cx, "options", "from", args[1]);
    if (!options) {
      return false;
    }
  }

  // Step 2.
  if (args.get(0).isObject()) {
    JSObject* item = &args[0].toObject();

    if (auto* yearMonth = item->maybeUnwrapIf<PlainYearMonthObject>()) {
      auto date = ToPlainDate(yearMonth);

      Rooted<JSObject*> calendar(cx, yearMonth->calendar());
      if (!cx->compartment()->wrap(cx, &calendar)) {
        return false;
      }

      if (options) {
        // Step 2.a.
        TemporalOverflow ignored;
        if (!ToTemporalOverflow(cx, options, &ignored)) {
          return false;
        }
      }

      // Step 2.b.
      auto* obj = CreateTemporalYearMonth(cx, date, calendar);
      if (!obj) {
        return false;
      }

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

  // Step 3.
  auto obj = ToTemporalYearMonth(cx, args.get(0), options);
  if (!obj) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.compare ( one, two )
 */
static bool PlainYearMonth_compare(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // Step 1.
  PlainDate one;
  if (!ToTemporalYearMonth(cx, args.get(0), &one)) {
    return false;
  }

  // Step 2.
  PlainDate two;
  if (!ToTemporalYearMonth(cx, args.get(1), &two)) {
    return false;
  }

  // Step 3.
  args.rval().setInt32(CompareISODate(one, two));
  return true;
}

/**
 * get Temporal.PlainYearMonth.prototype.calendar
 */
static bool PlainYearMonth_calendar(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  args.rval().setObject(*yearMonth->calendar());
  return true;
}

/**
 * get Temporal.PlainYearMonth.prototype.calendar
 */
static bool PlainYearMonth_calendar(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_calendar>(cx,
                                                                         args);
}

/**
 * get Temporal.PlainYearMonth.prototype.year
 */
static bool PlainYearMonth_year(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarYear(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.year
 */
static bool PlainYearMonth_year(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_year>(cx, args);
}

/**
 * get Temporal.PlainYearMonth.prototype.month
 */
static bool PlainYearMonth_month(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarMonth(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.month
 */
static bool PlainYearMonth_month(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_month>(cx, args);
}

/**
 * get Temporal.PlainYearMonth.prototype.monthCode
 */
static bool PlainYearMonth_monthCode(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarMonthCode(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.monthCode
 */
static bool PlainYearMonth_monthCode(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_monthCode>(cx,
                                                                          args);
}

/**
 * get Temporal.PlainYearMonth.prototype.daysInYear
 */
static bool PlainYearMonth_daysInYear(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarDaysInYear(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.daysInYear
 */
static bool PlainYearMonth_daysInYear(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_daysInYear>(
      cx, args);
}

/**
 * get Temporal.PlainYearMonth.prototype.daysInMonth
 */
static bool PlainYearMonth_daysInMonth(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarDaysInMonth(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.daysInMonth
 */
static bool PlainYearMonth_daysInMonth(JSContext* cx, unsigned argc,
                                       Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_daysInMonth>(
      cx, args);
}

/**
 * get Temporal.PlainYearMonth.prototype.monthsInYear
 */
static bool PlainYearMonth_monthsInYear(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarMonthsInYear(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.monthsInYear
 */
static bool PlainYearMonth_monthsInYear(JSContext* cx, unsigned argc,
                                        Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_monthsInYear>(
      cx, args);
}

/**
 * get Temporal.PlainYearMonth.prototype.inLeapYear
 */
static bool PlainYearMonth_inLeapYear(JSContext* cx, const CallArgs& args) {
  // Step 3.
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 4.
  return CalendarInLeapYear(cx, calendar, args.thisv(), args.rval());
}

/**
 * get Temporal.PlainYearMonth.prototype.inLeapYear
 */
static bool PlainYearMonth_inLeapYear(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_inLeapYear>(
      cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.with ( temporalYearMonthLike [ , options ]
 * )
 */
static bool PlainYearMonth_with(JSContext* cx, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 3.
  Rooted<JSObject*> temporalYearMonthLike(
      cx, RequireObjectArg(cx, "temporalYearMonthLike", "with", args.get(0)));
  if (!temporalYearMonthLike) {
    return false;
  }

  // Step 4.
  if (!RejectObjectWithCalendarOrTimeZone(cx, temporalYearMonthLike)) {
    return false;
  }

  // Step 5.
  Rooted<JSObject*> options(cx);
  if (args.hasDefined(1)) {
    options = RequireObjectArg(cx, "options", "with", args[1]);
  } else {
    options = NewPlainObjectWithProto(cx, nullptr);
  }
  if (!options) {
    return false;
  }

  // Step 6.
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 7.
  JS::RootedVector<PropertyKey> fieldNames(cx);
  if (!CalendarFields(
          cx, calendar,
          {CalendarField::Month, CalendarField::MonthCode, CalendarField::Year},
          &fieldNames)) {
    return false;
  }

  // Step 8.
  Rooted<PlainObject*> fields(cx,
                              PrepareTemporalFields(cx, yearMonth, fieldNames));
  if (!fields) {
    return false;
  }

  // Step 9.
  Rooted<PlainObject*> partialYearMonth(
      cx, PreparePartialTemporalFields(cx, temporalYearMonthLike, fieldNames));
  if (!partialYearMonth) {
    return false;
  }

  // Step 10.
  Rooted<JSObject*> mergedFields(
      cx, CalendarMergeFields(cx, calendar, fields, partialYearMonth));
  if (!mergedFields) {
    return false;
  }

  // Step 11.
  fields = PrepareTemporalFields(cx, mergedFields, fieldNames);
  if (!fields) {
    return false;
  }

  // Step 12.
  auto obj = CalendarYearMonthFromFields(cx, calendar, fields, options);
  if (!obj) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.prototype.with ( temporalYearMonthLike [ , options ]
 * )
 */
static bool PlainYearMonth_with(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_with>(cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.add ( temporalDurationLike [ , options ] )
 */
static bool PlainYearMonth_add(JSContext* cx, const CallArgs& args) {
  // Step 3.
  return AddDurationToOrSubtractDurationFromPlainYearMonth(
      cx, PlainYearMonthDuration::Add, args);
}

/**
 * Temporal.PlainYearMonth.prototype.add ( temporalDurationLike [ , options ] )
 */
static bool PlainYearMonth_add(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_add>(cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.subtract ( temporalDurationLike [ , options
 * ] )
 */
static bool PlainYearMonth_subtract(JSContext* cx, const CallArgs& args) {
  // Step 3.
  return AddDurationToOrSubtractDurationFromPlainYearMonth(
      cx, PlainYearMonthDuration::Subtract, args);
}

/**
 * Temporal.PlainYearMonth.prototype.subtract ( temporalDurationLike [ , options
 * ] )
 */
static bool PlainYearMonth_subtract(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_subtract>(cx,
                                                                         args);
}

/**
 * Temporal.PlainYearMonth.prototype.until ( other [ , options ] )
 */
static bool PlainYearMonth_until(JSContext* cx, const CallArgs& args) {
  // Step 3.
  return DifferenceTemporalPlainYearMonth(cx, TemporalDifference::Until, args);
}

/**
 * Temporal.PlainYearMonth.prototype.until ( other [ , options ] )
 */
static bool PlainYearMonth_until(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_until>(cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.since ( other [ , options ] )
 */
static bool PlainYearMonth_since(JSContext* cx, const CallArgs& args) {
  // Step 3.
  return DifferenceTemporalPlainYearMonth(cx, TemporalDifference::Since, args);
}

/**
 * Temporal.PlainYearMonth.prototype.since ( other [ , options ] )
 */
static bool PlainYearMonth_since(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_since>(cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.equals ( other )
 */
static bool PlainYearMonth_equals(JSContext* cx, const CallArgs& args) {
  auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>();
  auto date = ToPlainDate(yearMonth);
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 3.
  PlainDate other;
  Rooted<JSObject*> otherCalendar(cx);
  if (!ToTemporalYearMonth(cx, args.get(0), &other, &otherCalendar)) {
    return false;
  }

  // Steps 4-7.
  bool equals = false;
  if (date.year == other.year && date.month == other.month &&
      date.day == other.day) {
    if (!CalendarEquals(cx, calendar, otherCalendar, &equals)) {
      return false;
    }
  }

  args.rval().setBoolean(equals);
  return true;
}

/**
 * Temporal.PlainYearMonth.prototype.equals ( other )
 */
static bool PlainYearMonth_equals(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_equals>(cx,
                                                                       args);
}

/**
 * Temporal.PlainYearMonth.prototype.toString ( [ options ] )
 */
static bool PlainYearMonth_toString(JSContext* cx, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  auto showCalendar = CalendarOption::Auto;
  if (args.hasDefined(0)) {
    // Step 3.
    Rooted<JSObject*> options(
        cx, RequireObjectArg(cx, "options", "toString", args[0]));
    if (!options) {
      return false;
    }

    // Step 4.
    if (!ToCalendarNameOption(cx, options, &showCalendar)) {
      return false;
    }
  }

  // Step 5.
  JSString* str = TemporalYearMonthToString(cx, yearMonth, showCalendar);
  if (!str) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.prototype.toString ( [ options ] )
 */
static bool PlainYearMonth_toString(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toString>(cx,
                                                                         args);
}

/**
 * Temporal.PlainYearMonth.prototype.toLocaleString ( [ locales [ , options ] ]
 * )
 */
static bool PlainYearMonth_toLocaleString(JSContext* cx, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 3.
  JSString* str =
      TemporalYearMonthToString(cx, yearMonth, CalendarOption::Auto);
  if (!str) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.prototype.toLocaleString ( [ locales [ , options ] ]
 * )
 */
static bool PlainYearMonth_toLocaleString(JSContext* cx, unsigned argc,
                                          Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toLocaleString>(
      cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.toJSON ( )
 */
static bool PlainYearMonth_toJSON(JSContext* cx, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 3.
  JSString* str =
      TemporalYearMonthToString(cx, yearMonth, CalendarOption::Auto);
  if (!str) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.prototype.toJSON ( )
 */
static bool PlainYearMonth_toJSON(JSContext* cx, unsigned argc, Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toJSON>(cx,
                                                                       args);
}

/**
 *  Temporal.PlainYearMonth.prototype.valueOf ( )
 */
static bool PlainYearMonth_valueOf(JSContext* cx, unsigned argc, Value* vp) {
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO,
                            "PlainYearMonth", "primitive type");
  return false;
}

/**
 * Temporal.PlainYearMonth.prototype.toPlainDate ( item )
 */
static bool PlainYearMonth_toPlainDate(JSContext* cx, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 3.
  Rooted<JSObject*> item(
      cx, RequireObjectArg(cx, "item", "toPlainDate", args.get(0)));
  if (!item) {
    return false;
  }

  // Step 4.
  Rooted<JSObject*> calendar(cx, yearMonth->calendar());

  // Step 5.
  JS::RootedVector<PropertyKey> receiverFieldNames(cx);
  if (!CalendarFields(cx, calendar,
                      {CalendarField::MonthCode, CalendarField::Year},
                      &receiverFieldNames)) {
    return false;
  }

  // Step 6.
  Rooted<PlainObject*> fields(
      cx, PrepareTemporalFields(cx, yearMonth, receiverFieldNames));
  if (!fields) {
    return false;
  }

  // Step 7.
  JS::RootedVector<PropertyKey> inputFieldNames(cx);
  if (!CalendarFields(cx, calendar, {CalendarField::Day}, &inputFieldNames)) {
    return false;
  }

  // Step 8.
  Rooted<PlainObject*> inputFields(
      cx, PrepareTemporalFields(cx, item, inputFieldNames));
  if (!inputFields) {
    return false;
  }

  // Step 9.
  Rooted<JSObject*> mergedFields(
      cx, CalendarMergeFields(cx, calendar, fields, inputFields));
  if (!mergedFields) {
    return false;
  }

  // Step 10.
  JS::RootedVector<PropertyKey> mergedFieldNames(cx);
  if (!MergeTemporalFieldNames(receiverFieldNames, inputFieldNames,
                               mergedFieldNames.get())) {
    return false;
  }

  // Step 11.
  mergedFields = PrepareTemporalFields(cx, mergedFields, mergedFieldNames);
  if (!mergedFields) {
    return false;
  }

  // Step 12.
  Rooted<JSObject*> options(cx, NewPlainObjectWithProto(cx, nullptr));
  if (!options) {
    return false;
  }

  // Step 13.
  Rooted<Value> overflow(cx, StringValue(cx->names().reject));
  if (!DefineDataProperty(cx, options, cx->names().overflow, overflow)) {
    return false;
  }

  // Step 14.
  auto obj = CalendarDateFromFields(cx, calendar, mergedFields, options);
  if (!obj) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.prototype.toPlainDate ( item )
 */
static bool PlainYearMonth_toPlainDate(JSContext* cx, unsigned argc,
                                       Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toPlainDate>(
      cx, args);
}

/**
 * Temporal.PlainYearMonth.prototype.getISOFields ( )
 */
static bool PlainYearMonth_getISOFields(JSContext* cx, const CallArgs& args) {
  Rooted<PlainYearMonthObject*> yearMonth(
      cx, &args.thisv().toObject().as<PlainYearMonthObject>());

  // Step 3.
  Rooted<IdValueVector> fields(cx, IdValueVector(cx));

  // Step 4.
  if (!fields.emplaceBack(NameToId(cx->names().calendar),
                          ObjectValue(*yearMonth->calendar()))) {
    return false;
  }

  // Step 5.
  if (!fields.emplaceBack(NameToId(cx->names().isoDay),
                          Int32Value(yearMonth->isoDay()))) {
    return false;
  }

  // Step 6.
  if (!fields.emplaceBack(NameToId(cx->names().isoMonth),
                          Int32Value(yearMonth->isoMonth()))) {
    return false;
  }

  // Step 7.
  if (!fields.emplaceBack(NameToId(cx->names().isoYear),
                          Int32Value(yearMonth->isoYear()))) {
    return false;
  }

  // Step 8.
  auto* obj =
      NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length());
  if (!obj) {
    return false;
  }

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

/**
 * Temporal.PlainYearMonth.prototype.getISOFields ( )
 */
static bool PlainYearMonth_getISOFields(JSContext* cx, unsigned argc,
                                        Value* vp) {
  // Steps 1-2.
  CallArgs args = CallArgsFromVp(argc, vp);
  return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_getISOFields>(
      cx, args);
}

const JSClass PlainYearMonthObject::class_ = {
    "Temporal.PlainYearMonth",
    JSCLASS_HAS_RESERVED_SLOTS(PlainYearMonthObject::SLOT_COUNT) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_PlainYearMonth),
    JS_NULL_CLASS_OPS,
    &PlainYearMonthObject::classSpec_,
};

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

static const JSFunctionSpec PlainYearMonth_methods[] = {
    JS_FN("from", PlainYearMonth_from, 1, 0),
    JS_FN("compare", PlainYearMonth_compare, 2, 0),
    JS_FS_END,
};

static const JSFunctionSpec PlainYearMonth_prototype_methods[] = {
    JS_FN("with", PlainYearMonth_with, 1, 0),
    JS_FN("add", PlainYearMonth_add, 1, 0),
    JS_FN("subtract", PlainYearMonth_subtract, 1, 0),
    JS_FN("until", PlainYearMonth_until, 1, 0),
    JS_FN("since", PlainYearMonth_since, 1, 0),
    JS_FN("equals", PlainYearMonth_equals, 1, 0),
    JS_FN("toString", PlainYearMonth_toString, 0, 0),
    JS_FN("toLocaleString", PlainYearMonth_toLocaleString, 0, 0),
    JS_FN("toJSON", PlainYearMonth_toJSON, 0, 0),
    JS_FN("valueOf", PlainYearMonth_valueOf, 0, 0),
    JS_FN("toPlainDate", PlainYearMonth_toPlainDate, 1, 0),
    JS_FN("getISOFields", PlainYearMonth_getISOFields, 0, 0),
    JS_FS_END,
};

static const JSPropertySpec PlainYearMonth_prototype_properties[] = {
    JS_PSG("calendar", PlainYearMonth_calendar, 0),
    JS_PSG("year", PlainYearMonth_year, 0),
    JS_PSG("month", PlainYearMonth_month, 0),
    JS_PSG("monthCode", PlainYearMonth_monthCode, 0),
    JS_PSG("daysInYear", PlainYearMonth_daysInYear, 0),
    JS_PSG("daysInMonth", PlainYearMonth_daysInMonth, 0),
    JS_PSG("monthsInYear", PlainYearMonth_monthsInYear, 0),
    JS_PSG("inLeapYear", PlainYearMonth_inLeapYear, 0),
    JS_STRING_SYM_PS(toStringTag, "Temporal.PlainYearMonth", JSPROP_READONLY),
    JS_PS_END,
};

const ClassSpec PlainYearMonthObject::classSpec_ = {
    GenericCreateConstructor<PlainYearMonthConstructor, 2,
                             gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<PlainYearMonthObject>,
    PlainYearMonth_methods,
    nullptr,
    PlainYearMonth_prototype_methods,
    PlainYearMonth_prototype_properties,
    nullptr,
    ClassSpec::DontDefineConstructor,
};
