import { bootstrapPrototype } from '../bootstrap.mts';
import { GetOptionsObject } from '../../abstract-ops/temporal/addition.mts';
import type { TimeZoneIdentifier } from '../../abstract-ops/temporal/addition.mts';
import type { TemporalPlainDateObject } from './PlainDate.mts';
import {
  AddDurationToDate,
  CalendarDateFromFields,
  CalendarEquals,
  CalendarISOToDate,
  CalendarMergeFields,
  CalendarMonthDayFromFields,
  CalendarYearMonthFromFields,
  CompareISODate,
  CombineISODateAndTimeRecord,
  CreateTemporalDate,
  CreateTemporalDateTime,
  CreateTemporalMonthDay,
  CreateTemporalYearMonth,
  CreateTemporalZonedDateTime,
  DifferenceTemporalPlainDate,
  Get,
  GetEpochNanosecondsFor,
  GetStartOfDay,
  GetTemporalOverflowOption,
  GetTemporalShowCalendarNameOption,
  ISODateTimeWithinLimits,
  ISODateToFields,
  IsPartialTemporalObject,
  PrepareCalendarFields,
  Q,
  RequireInternalSlot,
  Throw,
  ToTemporalCalendarIdentifier,
  ToTemporalDate,
  ToTemporalTime,
  ToTemporalTimeZoneIdentifier,
  ToTimeRecordOrMidnight,
  Value,
  X,
  F,
  type Arguments,
  type FunctionCallContext,
  type PlainCompletion,
  type Realm,
  type ValueEvaluator,
  ObjectValue,
  TemporalDateToString,
} from '#self';

function thisTemporalDateValue(value: Value): PlainCompletion<TemporalPlainDateObject> {
  Q(RequireInternalSlot(value, 'InitializedTemporalDate'));
  return value as TemporalPlainDateObject;
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.calendarid */
function PlainDateProto_calendarIdGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Value(plainDate.Calendar);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.era */
function PlainDateProto_eraGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Value(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).Era);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.erayear */
function PlainDateProto_eraYearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const result = CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).EraYear;
  return result === undefined ? Value.undefined : F(result);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.year */
function PlainDateProto_yearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).Year);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.month */
function PlainDateProto_monthGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).Month);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.monthcode */
function PlainDateProto_monthCodeGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Value(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).MonthCode);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.day */
function PlainDateProto_dayGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).Day);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.dayofweek */
function PlainDateProto_dayOfWeekGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).DayOfWeek);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.dayofyear */
function PlainDateProto_dayOfYearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).DayOfYear);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.weekofyear */
function PlainDateProto_weekOfYearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const result = CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).WeekOfYear.Week;
  return result === undefined ? Value.undefined : F(result);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.yearofweek */
function PlainDateProto_yearOfWeekGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const result = CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).WeekOfYear.Year;
  return result === undefined ? Value.undefined : F(result);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.daysinweek */
function PlainDateProto_daysInWeekGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).DaysInWeek);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.daysinmonth */
function PlainDateProto_daysInMonthGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).DaysInMonth);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.daysinyear */
function PlainDateProto_daysInYearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).DaysInYear);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.monthsinyear */
function PlainDateProto_monthsInYearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return F(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).MonthsInYear);
}

/** https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.inleapyear */
function PlainDateProto_inLeapYearGetter(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Value(CalendarISOToDate(plainDate.Calendar, plainDate.ISODate).InLeapYear);
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.toplainyearmonth */
function* PlainDateProto_toPlainYearMonth(_args: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const calendar = plainDate.Calendar;
  const fields = ISODateToFields(calendar, plainDate.ISODate, 'date');
  const isoDate = Q(yield* CalendarYearMonthFromFields(calendar, fields, 'constrain'));
  return X(CreateTemporalYearMonth(isoDate, calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.toplainmonthday */
function* PlainDateProto_toPlainMonthDay(_args: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const calendar = plainDate.Calendar;
  const fields = ISODateToFields(calendar, plainDate.ISODate, 'date');
  const isoDate = Q(yield* CalendarMonthDayFromFields(calendar, fields, 'constrain'));
  return X(CreateTemporalMonthDay(isoDate, calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.add */
function* PlainDateProto_add([temporalDurationLike = Value.undefined, options = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Q(yield* AddDurationToDate('add', plainDate, temporalDurationLike, options));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.subtract */
function* PlainDateProto_subtract([temporalDurationLike = Value.undefined, options = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Q(yield* AddDurationToDate('subtract', plainDate, temporalDurationLike, options));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.with */
function* PlainDateProto_with([temporalDateLike = Value.undefined, options = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  if (!Q(yield* IsPartialTemporalObject(temporalDateLike))) {
    return Throw.TypeError('$1 is not a partial Temporal object', temporalDateLike);
  }
  const calendar = plainDate.Calendar;
  let fields = ISODateToFields(calendar, plainDate.ISODate, 'date');
  const partialDate = Q(yield* PrepareCalendarFields(calendar, temporalDateLike as ObjectValue, ['year', 'month', 'month-code', 'day'], [], 'partial'));
  fields = CalendarMergeFields(calendar, fields, partialDate);
  const resolvedOptions = Q(GetOptionsObject(options));
  const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
  const isoDate = Q(yield* CalendarDateFromFields(calendar, fields, overflow));
  return X(CreateTemporalDate(isoDate, calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.withcalendar */
function PlainDateProto_withCalendar([calendarLike = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const calendar = Q(ToTemporalCalendarIdentifier(calendarLike));
  return X(CreateTemporalDate(plainDate.ISODate, calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.until */
function* PlainDateProto_until([other = Value.undefined, options = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Q(yield* DifferenceTemporalPlainDate('until', plainDate, other, options));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.since */
function* PlainDateProto_since([other = Value.undefined, options = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Q(yield* DifferenceTemporalPlainDate('since', plainDate, other, options));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.equals */
function* PlainDateProto_equals([_other = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const other = Q(yield* ToTemporalDate(_other));
  if (CompareISODate(plainDate.ISODate, other.ISODate) !== 0) {
    return Value.false;
  }
  return Value(CalendarEquals(plainDate.Calendar, other.Calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.toplaindatetime */
function* PlainDateProto_toPlainDateTime([temporalTime = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const time = Q(yield* ToTimeRecordOrMidnight(temporalTime));
  const isoDateTime = CombineISODateAndTimeRecord(plainDate.ISODate, time);
  return Q(yield* CreateTemporalDateTime(isoDateTime, plainDate.Calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.tozoneddatetime */
function* PlainDateProto_toZonedDateTime([item = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  let timeZone: TimeZoneIdentifier;
  let temporalTime: Value;
  if (item instanceof ObjectValue) {
    const timeZoneLike = Q(yield* Get(item, Value('timeZone')));
    if (timeZoneLike === Value.undefined) {
      timeZone = Q(ToTemporalTimeZoneIdentifier(item));
      temporalTime = Value.undefined;
    } else {
      timeZone = Q(ToTemporalTimeZoneIdentifier(timeZoneLike));
      temporalTime = Q(yield* Get(item, Value('plainTime')));
    }
  } else {
    timeZone = Q(ToTemporalTimeZoneIdentifier(item));
    temporalTime = Value.undefined;
  }
  let epochNs: bigint;
  if (temporalTime === Value.undefined) {
    epochNs = Q(GetStartOfDay(timeZone, plainDate.ISODate));
  } else {
    const temporalTime2 = Q(yield* ToTemporalTime(temporalTime));
    const isoDateTime = CombineISODateAndTimeRecord(plainDate.ISODate, temporalTime2.Time);
    if (!ISODateTimeWithinLimits(isoDateTime)) {
      return Throw.RangeError('DateTime outside of range');
    }
    epochNs = Q(GetEpochNanosecondsFor(timeZone, isoDateTime, 'compatible'));
  }
  return X(CreateTemporalZonedDateTime(epochNs, timeZone, plainDate.Calendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.tostring */
function* PlainDateProto_toString([options = Value.undefined]: Arguments, { thisValue }: FunctionCallContext): ValueEvaluator {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  const resolvedOptions = Q(GetOptionsObject(options));
  const showCalendar = Q(yield* GetTemporalShowCalendarNameOption(resolvedOptions));
  return Value(TemporalDateToString(plainDate, showCalendar));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.tolocalestring */
function PlainDateProto_toLocaleString(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Value(TemporalDateToString(plainDate, 'auto'));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.tojson */
function PlainDateProto_toJSON(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  const plainDate = Q(thisTemporalDateValue(thisValue));
  return Value(TemporalDateToString(plainDate, 'auto'));
}

/** https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.valueof */
function PlainDateProto_valueOf(_args: Arguments, { thisValue }: FunctionCallContext): PlainCompletion<Value> {
  Q(thisTemporalDateValue(thisValue));
  return Throw.TypeError('Temporal.PlainDate cannot be converted to primitive value. If you are comparing two Temporal.PlainDate objects with > or <, use Temporal.PlainDate.compare() instead.');
}

export function bootstrapTemporalPlainDatePrototype(realmRec: Realm) {
  const prototype = bootstrapPrototype(realmRec, [
    ['calendarId', [PlainDateProto_calendarIdGetter]],
    ['era', [PlainDateProto_eraGetter]],
    ['eraYear', [PlainDateProto_eraYearGetter]],
    ['year', [PlainDateProto_yearGetter]],
    ['month', [PlainDateProto_monthGetter]],
    ['monthCode', [PlainDateProto_monthCodeGetter]],
    ['day', [PlainDateProto_dayGetter]],
    ['dayOfWeek', [PlainDateProto_dayOfWeekGetter]],
    ['dayOfYear', [PlainDateProto_dayOfYearGetter]],
    ['weekOfYear', [PlainDateProto_weekOfYearGetter]],
    ['yearOfWeek', [PlainDateProto_yearOfWeekGetter]],
    ['daysInWeek', [PlainDateProto_daysInWeekGetter]],
    ['daysInMonth', [PlainDateProto_daysInMonthGetter]],
    ['daysInYear', [PlainDateProto_daysInYearGetter]],
    ['monthsInYear', [PlainDateProto_monthsInYearGetter]],
    ['inLeapYear', [PlainDateProto_inLeapYearGetter]],
    ['toPlainYearMonth', PlainDateProto_toPlainYearMonth, 0],
    ['toPlainMonthDay', PlainDateProto_toPlainMonthDay, 0],
    ['add', PlainDateProto_add, 1],
    ['subtract', PlainDateProto_subtract, 1],
    ['with', PlainDateProto_with, 1],
    ['withCalendar', PlainDateProto_withCalendar, 1],
    ['until', PlainDateProto_until, 1],
    ['since', PlainDateProto_since, 1],
    ['equals', PlainDateProto_equals, 1],
    ['toPlainDateTime', PlainDateProto_toPlainDateTime, 0],
    ['toZonedDateTime', PlainDateProto_toZonedDateTime, 1],
    ['toString', PlainDateProto_toString, 0],
    ['toLocaleString', PlainDateProto_toLocaleString, 0],
    ['toJSON', PlainDateProto_toJSON, 0],
    ['valueOf', PlainDateProto_valueOf, 0],
  ], realmRec.Intrinsics['%Object.prototype%'], 'Temporal.PlainDate');
  realmRec.Intrinsics['%Temporal.PlainDate.prototype%'] = prototype;
  return prototype;
}
