import { ParseDateTimeUTCOffset, ParseISODateTime } from '../../parser/TemporalParser.mts';
import { R } from '../spec-types.mjs';
import { type ISODateRecord, type TemporalPlainDateObject, isTemporalPlainDateObject } from '../../intrinsics/Temporal/PlainDate.mts';
import { isTemporalPlainDateTimeObject } from '../../intrinsics/Temporal/PlainDateTime.mts';
import { type TemporalZonedDateTimeObject, isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
import {
  GetOption, GetRoundingIncrementOption, GetRoundingModeOption, ToZeroPaddedDecimalString, UnsignedRoundingMode, type TimeZoneIdentifier,
} from './addition.mts';
import { RoundingMode } from './addition.mts';
import {
  CalendarISOToDate, CanonicalizeCalendar, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, type CalendarFieldsRecord, type CalendarType,
} from './calendar.mts';
import { ToTemporalTimeZoneIdentifier } from './time-zone.mts';
import {
  ToPrimitive, ToNumber, Throw, CreateISODateRecord, CreateTemporalDate, CreateTemporalZonedDateTime, InterpretISODateTimeOffset, InterpretTemporalDateTimeFields, nsPerDay, type ISODateTimeMatchBehaviour, type ISODateTimeOffsetBehaviour,
  Value, ObjectValue, JSStringValue, NumberValue, UndefinedValue, Q, surroundingAgent, Get, ToString, type PlainCompletion, type PlainEvaluator, Assert, type PropertyKeyValue, X,
} from '#self';

/** https://tc39.es/proposal-temporal/#sec-isodatetoepochdays */
// TODO(temporal): Review
export function ISODateToEpochDays(year: number, month: number, date: number): number {
  const resolvedYear = year + Math.floor(month / 12);
  const resolvedMonth = ((month % 12) + 12) % 12;
  // Find a time t such that EpochTimeToEpochYear(t) = resolvedYear, EpochTimeToMonthInYear(t) = resolvedMonth, and EpochTimeToDate(t) = 1.
  const y = resolvedYear;
  const m = resolvedMonth;
  let t = EpochDayNumberForYear(y);
  const isLeap = MathematicalDaysInYear(y) === 366;
  const monthDays = [
    31,
    isLeap ? 29 : 28,
    31, 30, 31, 30,
    31, 31, 30, 31, 30, 31,
  ];
  for (let i = 0; i < m; i += 1) {
    t += monthDays[i];
  }
  Assert(EpochTimeToEpochYear(t) === resolvedYear && EpochTimeToMonthInYear(t) === resolvedMonth && EpochTimeToDate(t) === 1);

  return EpochTimeToDayNumber(t) + date - 1;
}

/** https://tc39.es/proposal-temporal/#sec-epochdaystoepochms */
export function EpochDaysToEpochMs(day: number, time: number): number {
  return day * 86400000 + time;
}

/** https://tc39.es/proposal-temporal/#eqn-EpochTimeToDayNumber */
export function EpochTimeToDayNumber(t: number): number {
  return Math.floor(t / 86400000);
}

/** https://tc39.es/proposal-temporal/#sec-mathematicaldaysinyear */
export function MathematicalDaysInYear(y: number): number {
  if (y % 4 !== 0) {
    return 365;
  }
  if (y % 100 !== 0) {
    return 366;
  }
  if (y % 400 !== 0) {
    return 365;
  }
  return 366;
}

/** https://tc39.es/proposal-temporal/#sec-epochdaynumberforyear */
export function EpochDayNumberForYear(y: number): number {
  return 365 * (y - 1970)
    + Math.floor((y - 1969) / 4)
    - Math.floor((y - 1901) / 100)
    + Math.floor((y - 1601) / 400);
}

/** https://tc39.es/proposal-temporal/#sec-epochtimeforyear */
export function EpochTimeForYear(y: number): number {
  return 86400000 * EpochDayNumberForYear(y);
}

/** https://tc39.es/proposal-temporal/#sec-epochtimetoepochyear */
// TODO(temporal): Review
export function EpochTimeToEpochYear(t: number): number {
  // EpochTimeToEpochYear(t) = the largest integral Number y (closest to +∞) such that EpochTimeForYear(y) ≤ t
  let lower = -271821;
  let upper = 275760;
  while (lower < upper) {
    const mid = Math.floor((lower + upper + 1) / 2);
    if (EpochTimeForYear(mid) <= t) {
      lower = mid;
    } else {
      upper = mid - 1;
    }
  }
  return lower;
}

/** https://tc39.es/proposal-temporal/#sec-mathematicalinleapyear */
export function MathematicalInLeapYear(t: number): number {
  return MathematicalDaysInYear(EpochTimeToEpochYear(t)) === 366 ? 1 : 0;
}

/** https://tc39.es/proposal-temporal/#sec-epochtimetomonthinyear */
export function EpochTimeToMonthInYear(t: number): number {
  const dayInYear = EpochTimeToDayInYear(t);
  const leap = MathematicalInLeapYear(t);
  if (dayInYear >= 0 && dayInYear < 31) return 0;
  if (dayInYear >= 31 && dayInYear < 59 + leap) return 1;
  if (59 + leap <= dayInYear && dayInYear < 90 + leap) return 2;
  if (90 + leap <= dayInYear && dayInYear < 120 + leap) return 3;
  if (120 + leap <= dayInYear && dayInYear < 151 + leap) return 4;
  if (151 + leap <= dayInYear && dayInYear < 181 + leap) return 5;
  if (181 + leap <= dayInYear && dayInYear < 212 + leap) return 6;
  if (212 + leap <= dayInYear && dayInYear < 243 + leap) return 7;
  if (243 + leap <= dayInYear && dayInYear < 273 + leap) return 8;
  if (273 + leap <= dayInYear && dayInYear < 304 + leap) return 9;
  if (304 + leap <= dayInYear && dayInYear < 334 + leap) return 10;
  return 11;
}

/** https://tc39.es/proposal-temporal/#sec-epochtimetodayinyear */
export function EpochTimeToDayInYear(t: number): number {
  return EpochTimeToDayNumber(t) - EpochDayNumberForYear(EpochTimeToEpochYear(t));
}

/** https://tc39.es/proposal-temporal/#sec-epochtimetodate */
export function EpochTimeToDate(t: number): number {
  const m = EpochTimeToMonthInYear(t);
  const dayInYear = EpochTimeToDayInYear(t);
  const leap = MathematicalInLeapYear(t) ? 1 : 0;
  if (m === 0) return dayInYear + 1;
  if (m === 1) return dayInYear - 30;
  if (m === 2) return dayInYear - 58 - leap;
  if (m === 3) return dayInYear - 89 - leap;
  if (m === 4) return dayInYear - 119 - leap;
  if (m === 5) return dayInYear - 150 - leap;
  if (m === 6) return dayInYear - 180 - leap;
  if (m === 7) return dayInYear - 211 - leap;
  if (m === 8) return dayInYear - 242 - leap;
  if (m === 9) return dayInYear - 272 - leap;
  if (m === 10) return dayInYear - 303 - leap;
  return dayInYear - 333 - leap;
}

/** https://tc39.es/proposal-temporal/#sec-epochtimetoweekday */
export function EpochTimeToWeekDay(t: number): number {
  return (EpochTimeToDayNumber(t) + 4) % 7;
}

/** https://tc39.es/proposal-temporal/#sec-checkisodaysrange */
export function CheckISODaysRange(isoDate: ISODateRecord): PlainCompletion<void> {
  const days = Math.abs(ISODateToEpochDays(isoDate.Year, isoDate.Month - 1, isoDate.Day));
  if (days > 1e8) {
    return Throw.RangeError('ISODate is out of range');
  }
  return undefined;
}

/** https://tc39.es/proposal-temporal/#sec-temporal-units */
export enum TemporalUnit {
  Year, Month, Week, Day,
  Hour, Minute, Second, Millisecond, Microsecond, Nanosecond
}

/** https://tc39.es/proposal-temporal/#table-temporal-units */
export type TimeUnit = TemporalUnit.Hour | TemporalUnit.Minute | TemporalUnit.Second | TemporalUnit.Millisecond | TemporalUnit.Microsecond | TemporalUnit.Nanosecond;

export function __IsTimeUnit(unit: TemporalUnit): unit is TimeUnit {
  return (unit === TemporalUnit.Hour
    || unit === TemporalUnit.Minute
    || unit === TemporalUnit.Second
    || unit === TemporalUnit.Millisecond
    || unit === TemporalUnit.Microsecond
    || unit === TemporalUnit.Nanosecond
  );
}

/** https://tc39.es/proposal-temporal/#table-temporal-units */
export type DateUnit = TemporalUnit.Year | TemporalUnit.Month | TemporalUnit.Week | TemporalUnit.Day;

/** https://tc39.es/proposal-temporal/#table-temporal-units */
export const Table21_LengthInNanoSeconds = {
  [TemporalUnit.Day]: 8.64e13 satisfies typeof nsPerDay,
  [TemporalUnit.Hour]: 3.6e12,
  [TemporalUnit.Minute]: 6e10,
  [TemporalUnit.Second]: 1e9,
  [TemporalUnit.Millisecond]: 1e6,
  [TemporalUnit.Microsecond]: 1e3,
  [TemporalUnit.Nanosecond]: 1,
} as const;

export const Table21_CategoryByValue = {
  [TemporalUnit.Year]: 'date',
  [TemporalUnit.Month]: 'date',
  [TemporalUnit.Week]: 'date',
  [TemporalUnit.Day]: 'date',
  [TemporalUnit.Hour]: 'time',
  [TemporalUnit.Minute]: 'time',
  [TemporalUnit.Second]: 'time',
  [TemporalUnit.Millisecond]: 'time',
  [TemporalUnit.Microsecond]: 'time',
  [TemporalUnit.Nanosecond]: 'time',
} as const;

export function __IsDateUnit(unit: TemporalUnit): unit is DateUnit {
  return (unit === TemporalUnit.Year
    || unit === TemporalUnit.Month
    || unit === TemporalUnit.Week
    || unit === TemporalUnit.Day
  );
}

/** https://tc39.es/proposal-temporal/#sec-gettemporaloverflowoption */
export function* GetTemporalOverflowOption(options: ObjectValue): PlainEvaluator<'constrain' | 'reject'> {
  const stringValue = Q(yield* GetOption(options, 'overflow', 'string', ['constrain', 'reject'], 'constrain'));
  if (stringValue === 'constrain') {
    return 'constrain';
  }
  return 'reject';
}

/** https://tc39.es/proposal-temporal/#sec-gettemporaldisambiguationoption */
export function* GetTemporalDisambiguationOption(options: ObjectValue): PlainEvaluator<'compatible' | 'earlier' | 'later' | 'reject'> {
  const stringValue = Q(yield* GetOption(options, 'disambiguation', 'string', ['compatible', 'earlier', 'later', 'reject'], 'compatible'));
  if (stringValue === 'compatible') return 'compatible';
  if (stringValue === 'earlier') return 'earlier';
  if (stringValue === 'later') return 'later';
  return 'reject';
}

/** https://tc39.es/proposal-temporal/#sec-negateroundingmode */
export function NegateRoundingMode(roundingMode: RoundingMode): RoundingMode {
  switch (roundingMode) {
    case RoundingMode.Ceil: return RoundingMode.Floor;
    case RoundingMode.Floor: return RoundingMode.Ceil;
    case RoundingMode.HalfCeil: return RoundingMode.HalfFloor;
    case RoundingMode.HalfFloor: return RoundingMode.HalfCeil;
    default: return roundingMode;
  }
}

export type TemporalOffsetOption = 'prefer' | 'use' | 'ignore' | 'reject';
/** https://tc39.es/proposal-temporal/#sec-gettemporaloffsetoption */
export function* GetTemporalOffsetOption(options: ObjectValue, fallback: TemporalOffsetOption): PlainEvaluator<TemporalOffsetOption> {
  // step 1 to 4
  const stringFallback = fallback;
  const stringValue = Q(yield* GetOption(options, 'offset', 'string', ['prefer', 'use', 'ignore', 'reject'], stringFallback));
  if (stringValue === 'prefer') return 'prefer';
  if (stringValue === 'use') return 'use';
  if (stringValue === 'ignore') return 'ignore';
  return 'reject';
}

export type ShowCalendarNameOption = 'auto' | 'always' | 'never' | 'critical';
/** https://tc39.es/proposal-temporal/#sec-gettemporalshowcalendarnameoption */
export function* GetTemporalShowCalendarNameOption(options: ObjectValue): PlainEvaluator<ShowCalendarNameOption> {
  const stringValue = Q(yield* GetOption(options, 'calendarName', 'string', ['auto', 'always', 'never', 'critical'], 'auto'));
  if (stringValue === 'always') return 'always';
  if (stringValue === 'never') return 'never';
  if (stringValue === 'critical') return 'critical';
  return 'auto';
}

export type ShowTimeZoneNameOption = 'auto' | 'never' | 'critical';
/** https://tc39.es/proposal-temporal/#sec-gettemporalshowtimezonenameoption */
export function* GetTemporalShowTimeZoneNameOption(options: ObjectValue): PlainEvaluator<ShowTimeZoneNameOption> {
  const stringValue = Q(yield* GetOption(options, 'timeZoneName', 'string', ['auto', 'never', 'critical'], 'auto'));
  if (stringValue === 'never') return 'never';
  if (stringValue === 'critical') return 'critical';
  return 'auto';
}

/** https://tc39.es/proposal-temporal/#sec-gettemporalshowoffsetoption */
export function* GetTemporalShowOffsetOption(options: ObjectValue): PlainEvaluator<'auto' | 'never'> {
  const stringValue = Q(yield* GetOption(options, 'offset', 'string', ['auto', 'never'], 'auto'));
  if (stringValue === 'never') return 'never';
  return 'auto';
}

export type DirectionOption = 'next' | 'previous';
/** https://tc39.es/proposal-temporal/#sec-getdirectionoption */
export function* GetDirectionOption(options: ObjectValue): PlainEvaluator<DirectionOption> {
  const stringValue = Q(yield* GetOption(options, 'direction', 'string', ['next', 'previous'], '~required~'));
  if (stringValue === 'next') return 'next';
  return 'previous';
}

/** https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement */
export function ValidateTemporalRoundingIncrement(increment: number, dividend: number, inclusive: boolean): PlainCompletion<void> {
  let maximum;
  if (inclusive) {
    maximum = dividend;
  } else {
    Assert(dividend > 1);
    maximum = dividend - 1;
  }
  if (increment > maximum) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', increment);
  }
  if (dividend % increment !== 0) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', increment);
  }
  return undefined;
}

/** https://tc39.es/proposal-temporal/#sec-gettemporalfractionalseconddigitsoption */
export function* GetTemporalFractionalSecondDigitsOption(options: ObjectValue): PlainEvaluator<'auto' | number> {
  const digitsValue = Q(yield* Get(options, Value('fractionalSecondDigits')));
  if (digitsValue instanceof UndefinedValue) {
    return 'auto';
  }
  if (!(digitsValue instanceof NumberValue)) {
    if (Q(yield* ToString(digitsValue)).stringValue() !== 'auto') {
      return surroundingAgent.Throw('RangeError', 'OutOfRange', digitsValue);
    }
    return 'auto';
  }
  if (digitsValue.isNaN() || digitsValue.isInfinity()) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', digitsValue);
  }
  const digitCount = Math.floor(R(digitsValue));
  if (digitCount < 0 || digitCount > 9) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', digitsValue);
  }
  return digitCount;
}

/** https://tc39.es/proposal-temporal/#sec-tosecondsstringprecisionrecord */
export function ToSecondsStringPrecisionRecord(
  smallestUnit: Exclude<TimeUnit, TemporalUnit.Hour> | 'unset',
  fractionalDigitCount: 'auto' | number,
): {
  Precision: TemporalUnit.Minute | 'auto' | number,
  Unit: TemporalUnit.Minute | TemporalUnit.Second | TemporalUnit.Millisecond | TemporalUnit.Microsecond | TemporalUnit.Nanosecond,
  Increment: 1 | 10 | 100
} {
  if (smallestUnit === TemporalUnit.Minute) {
    return { Precision: TemporalUnit.Minute, Unit: TemporalUnit.Minute, Increment: 1 };
  }
  if (smallestUnit === TemporalUnit.Second) {
    return { Precision: 0, Unit: TemporalUnit.Second, Increment: 1 };
  }
  if (smallestUnit === TemporalUnit.Millisecond) {
    return { Precision: 3, Unit: TemporalUnit.Millisecond, Increment: 1 };
  }
  if (smallestUnit === TemporalUnit.Microsecond) {
    return { Precision: 6, Unit: TemporalUnit.Microsecond, Increment: 1 };
  }
  if (smallestUnit === TemporalUnit.Nanosecond) {
    return { Precision: 9, Unit: TemporalUnit.Nanosecond, Increment: 1 };
  }
  Assert(smallestUnit === 'unset');
  if (fractionalDigitCount === 'auto') {
    return { Precision: 'auto', Unit: TemporalUnit.Nanosecond, Increment: 1 };
  }
  if (fractionalDigitCount === 0) {
    return { Precision: 0, Unit: TemporalUnit.Second, Increment: 1 };
  }
  if (fractionalDigitCount >= 1 && fractionalDigitCount <= 3) {
    return { Precision: fractionalDigitCount, Unit: TemporalUnit.Millisecond, Increment: 10 ** (3 - fractionalDigitCount) as 1 | 10 | 100 };
  }
  if (fractionalDigitCount >= 4 && fractionalDigitCount <= 6) {
    return { Precision: fractionalDigitCount, Unit: TemporalUnit.Microsecond, Increment: 10 ** (6 - fractionalDigitCount) as 1 | 10 | 100 };
  }
  Assert(fractionalDigitCount >= 7 && fractionalDigitCount <= 9);
  return { Precision: fractionalDigitCount, Unit: TemporalUnit.Nanosecond, Increment: 10 ** (9 - fractionalDigitCount) as 1 | 10 | 100 };
}

const table21 = [
  {
    Value: TemporalUnit.Year, Singular: 'year', Plural: 'years',
  },
  {
    Value: TemporalUnit.Month, Singular: 'month', Plural: 'months',
  },
  {
    Value: TemporalUnit.Week, Singular: 'week', Plural: 'weeks',
  },
  {
    Value: TemporalUnit.Day, Singular: 'day', Plural: 'days',
  },
  {
    Value: TemporalUnit.Hour, Singular: 'hour', Plural: 'hours',
  },
  {
    Value: TemporalUnit.Minute, Singular: 'minute', Plural: 'minutes',
  },
  {
    Value: TemporalUnit.Second, Singular: 'second', Plural: 'seconds',
  },
  {
    Value: TemporalUnit.Millisecond, Singular: 'millisecond', Plural: 'milliseconds',
  },
  {
    Value: TemporalUnit.Microsecond, Singular: 'microsecond', Plural: 'microseconds',
  },
  {
    Value: TemporalUnit.Nanosecond, Singular: 'nanosecond', Plural: 'nanoseconds',
  },
] as const;
/** https://tc39.es/proposal-temporal/#sec-gettemporalunitvaluedoption */
export function* GetTemporalUnitValuedOption(
  options: ObjectValue,
  key: PropertyKeyValue | string,
  defaultV: 'required' | 'unset',
): PlainEvaluator<TemporalUnit | 'unset' | 'auto'> {
  // 1. Let allowedStrings be a List containing all values in the "Singular property name" and "Plural property name" columns of Table 21, except the header row.
  const allowedStrings = table21.map<string>((row) => row.Singular).concat(table21.map((row) => row.Plural)).concat('auto');
  const defaultValue = defaultV === 'unset' ? undefined : defaultV;
  const value = Q(yield* GetOption(options, key, 'string', allowedStrings, defaultValue));
  if (value === undefined) {
    return 'unset';
  }
  if (value === 'auto') {
    return 'auto';
  }
  // 9. Return the value in the "Value" column of Table 21 corresponding to the row with value in its "Singular property name" or "Plural property name" column.
  const returnValue = table21.find((row) => row.Singular === value || row.Plural === value)?.Value;
  Assert(returnValue !== undefined);
  return returnValue;
}

/** https://tc39.es/proposal-temporal/#sec-temporal-validatetemporalunitvaluedoption */
export function ValidateTemporalUnitValue(value: TemporalUnit | 'unset' | 'auto', unitGroup: 'date' | 'time' | 'datetime', extraValues?: Array<TemporalUnit | 'auto'>): PlainCompletion<void> {
  if (value === 'unset') return undefined;
  if (extraValues?.includes(value)) return undefined;
  const category = Table21_CategoryByValue[value as TemporalUnit];
  if (!category) {
    return Throw.RangeError('Invalid TemporalUnit value $1', value);
  }
  if (category === 'date' && (unitGroup === 'datetime' || unitGroup === 'date')) return undefined;
  if (category === 'time' && (unitGroup === 'datetime' || unitGroup === 'time')) return undefined;
  return Throw.RangeError('Invalid TemporalUnit value $1', value);
}

/** https://tc39.es/proposal-temporal/#sec-gettemporalrelativetooption */
export function* GetTemporalRelativeToOption(options: ObjectValue): PlainEvaluator<{
  PlainRelativeTo?: TemporalPlainDateObject,
  ZonedRelativeTo?: TemporalZonedDateTimeObject,
}> {
  const value = Q(yield* Get(options, Value('relativeTo')));
  if (value instanceof UndefinedValue) {
    return { PlainRelativeTo: undefined, ZonedRelativeTo: undefined };
  }
  let offsetBehaviour: ISODateTimeOffsetBehaviour = 'option';
  let matchBehaviour: ISODateTimeMatchBehaviour = 'match-exactly';
  let timeZone: TimeZoneIdentifier | 'unset';
  let isoDate;
  let time;
  let calendar: CalendarType | undefined;
  let offsetString;
  if (value instanceof ObjectValue) {
    if (isTemporalZonedDateTimeObject(value)) {
      return { PlainRelativeTo: undefined, ZonedRelativeTo: value };
    }
    if (isTemporalPlainDateObject(value)) {
      return { PlainRelativeTo: value, ZonedRelativeTo: undefined };
    }
    if (isTemporalPlainDateTimeObject(value)) {
      const plainDate = X(CreateTemporalDate(value.ISODateTime.ISODate, value.Calendar));
      return { PlainRelativeTo: plainDate, ZonedRelativeTo: undefined };
    }
    calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(value));
    const fields = Q(yield* PrepareCalendarFields(calendar, value, ['year', 'month', 'month-code', 'day'], ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'offset', 'time-zone'], []));
    const result = Q(yield* InterpretTemporalDateTimeFields(calendar, fields, 'constrain'));
    timeZone = fields.TimeZone as TimeZoneIdentifier;
    offsetString = fields.OffsetString;
    if (offsetString === undefined) {
      offsetBehaviour = 'wall';
    }
    isoDate = result.ISODate;
    time = result.Time;
  } else {
    if (!(value instanceof JSStringValue)) {
      return surroundingAgent.Throw('TypeError', 'NotAString', value);
    }
    const result = Q(ParseISODateTime(value.stringValue(), ['TemporalDateTimeString[+Zoned]', 'TemporalDateTimeString[~Zoned]']));
    offsetString = result.TimeZone.OffsetString;
    const annotation = result.TimeZone.TimeZoneAnnotation;
    if (!annotation) {
      timeZone = 'unset';
    } else {
      timeZone = Q(ToTemporalTimeZoneIdentifier(annotation));
      if (result.TimeZone.Z === true) {
        offsetBehaviour = 'exact';
      } else if (!offsetString) {
        offsetBehaviour = 'wall';
      }
      matchBehaviour = 'match-minutes';
    }
    let _calendar = result.Calendar;
    if (!_calendar) {
      _calendar = 'iso8601';
    }
    calendar = Q(CanonicalizeCalendar(_calendar));
    isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
    time = result.Time;
  }
  if (timeZone === 'unset') {
    const plainDate = Q(yield* CreateTemporalDate(isoDate, calendar));
    return { PlainRelativeTo: plainDate, ZonedRelativeTo: undefined };
  }
  let offsetNs;
  if (offsetBehaviour === 'option') {
    offsetNs = X(ParseDateTimeUTCOffset(offsetString!));
  } else {
    offsetNs = 0;
  }
  const epochNanoseconds = Q(InterpretISODateTimeOffset(isoDate, time, offsetBehaviour, offsetNs, timeZone, 'compatible', 'reject', matchBehaviour));
  const zonedRelativeTo = X(CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar));
  return { PlainRelativeTo: undefined, ZonedRelativeTo: zonedRelativeTo };
}

/** https://tc39.es/proposal-temporal/#sec-largeroftwotemporalunits */
export function LargerOfTwoTemporalUnits(u1: TemporalUnit, u2: TemporalUnit): TemporalUnit {
  const order = [
    TemporalUnit.Year,
    TemporalUnit.Month,
    TemporalUnit.Week,
    TemporalUnit.Day,
    TemporalUnit.Hour,
    TemporalUnit.Minute,
    TemporalUnit.Second,
    TemporalUnit.Millisecond,
    TemporalUnit.Microsecond,
    TemporalUnit.Nanosecond,
  ];
  for (const unit of order) {
    if (u1 === unit) {
      return unit;
    }
    if (u2 === unit) {
      return unit;
    }
  }
  Assert(false, 'unreachable');
}

/** https://tc39.es/proposal-temporal/#sec-iscalendarunit */
export function IsCalendarUnit(unit: TemporalUnit): unit is TemporalUnit.Year | TemporalUnit.Month | TemporalUnit.Week {
  return unit === TemporalUnit.Year || unit === TemporalUnit.Month || unit === TemporalUnit.Week;
}

/** https://tc39.es/proposal-temporal/#sec-temporalunitcategory */
export function TemporalUnitCategory(unit: TemporalUnit): 'date' | 'time' {
  if (unit === TemporalUnit.Year || unit === TemporalUnit.Month || unit === TemporalUnit.Week || unit === TemporalUnit.Day) {
    return 'date';
  }
  return 'time';
}

/** https://tc39.es/proposal-temporal/#sec-maximumtemporaldurationroundingincrement */
export function MaximumTemporalDurationRoundingIncrement(unit: TemporalUnit): 24 | 60 | 1000 | 'unset' {
  switch (unit) {
    case TemporalUnit.Hour: return 24;
    case TemporalUnit.Minute: return 60;
    case TemporalUnit.Second: return 60;
    case TemporalUnit.Millisecond: return 1000;
    case TemporalUnit.Microsecond: return 1000;
    case TemporalUnit.Nanosecond: return 1000;
    default: return 'unset';
  }
}

/** https://tc39.es/proposal-temporal/#sec-ispartialtemporalobject */
export function* IsPartialTemporalObject(value: Value): PlainEvaluator<boolean> {
  if (!(value instanceof ObjectValue)) {
    return false;
  }
  if (
    'InitializedTemporalDate' in value
    || 'InitializedTemporalDateTime' in value
    || 'InitializedTemporalMonthDay' in value
    || 'InitializedTemporalTime' in value
    || 'InitializedTemporalYearMonth' in value
    || 'InitializedTemporalZonedDateTime' in value
  ) {
    return false;
  }
  const calendarProperty = Q(yield* Get(value, Value('calendar')));
  if (!(calendarProperty instanceof UndefinedValue)) {
    return false;
  }
  const timeZoneProperty = Q(yield* Get(value, Value('timeZone')));
  if (!(timeZoneProperty instanceof UndefinedValue)) {
    return false;
  }
  return true;
}

/** https://tc39.es/proposal-temporal/#sec-formatfractionalseconds */
export function FormatFractionalSeconds(subSecondNanoseconds: number, precision: number | 'auto'): string {
  if (precision === 'auto') {
    if (subSecondNanoseconds === 0) {
      return '';
    }
    let fractionString = ToZeroPaddedDecimalString(subSecondNanoseconds, 9);
    // Set fractionString to the longest prefix of fractionString ending with a code unit other than 0x0030 (DIGIT ZERO).
    fractionString = fractionString.replace(/0+$/, '');
    return `.${fractionString}`;
  } else {
    if (precision === 0) {
      return '';
    }
    let fractionString = ToZeroPaddedDecimalString(subSecondNanoseconds, 9);
    fractionString = fractionString.slice(0, precision);
    return `.${fractionString}`;
  }
}

/** https://tc39.es/proposal-temporal/#sec-formattimestring */
export function FormatTimeString(
  hour: number,
  minute: number,
  second: number,
  subSecondNanoseconds: number,
  precision: number | 'minute' | 'auto',
  style?: 'separated' | 'unseparated',
): string {
  const separator = style === 'unseparated' ? '' : ':';
  const hh = ToZeroPaddedDecimalString(hour, 2);
  const mm = ToZeroPaddedDecimalString(minute, 2);
  if (precision === 'minute') {
    return hh + separator + mm;
  }
  const ss = ToZeroPaddedDecimalString(second, 2);
  const subSecondsPart = FormatFractionalSeconds(subSecondNanoseconds, precision);
  return hh + separator + mm + separator + ss + subSecondsPart;
}

/** https://tc39.es/proposal-temporal/#sec-getunsignedroundingmode */
export function GetUnsignedRoundingMode(
  roundingMode: RoundingMode,
  sign: 'negative' | 'positive',
): UnsignedRoundingMode {
  const table = {
    [RoundingMode.Ceil]: { positive: UnsignedRoundingMode.Infinity, negative: UnsignedRoundingMode.Zero },
    [RoundingMode.Floor]: { positive: UnsignedRoundingMode.Zero, negative: UnsignedRoundingMode.Infinity },
    [RoundingMode.Expand]: { positive: UnsignedRoundingMode.Infinity, negative: UnsignedRoundingMode.Infinity },
    [RoundingMode.Trunc]: { positive: UnsignedRoundingMode.Zero, negative: UnsignedRoundingMode.Zero },
    [RoundingMode.HalfCeil]: { positive: UnsignedRoundingMode.HalfInfinity, negative: UnsignedRoundingMode.HalfZero },
    [RoundingMode.HalfFloor]: { positive: UnsignedRoundingMode.HalfZero, negative: UnsignedRoundingMode.HalfInfinity },
    [RoundingMode.HalfExpand]: { positive: UnsignedRoundingMode.HalfInfinity, negative: UnsignedRoundingMode.HalfInfinity },
    [RoundingMode.HalfTrunc]: { positive: UnsignedRoundingMode.HalfZero, negative: UnsignedRoundingMode.HalfZero },
    [RoundingMode.HalfEven]: { positive: UnsignedRoundingMode.HalfEven, negative: UnsignedRoundingMode.HalfEven },
  } as const;
  return table[roundingMode][sign];
}

/** https://tc39.es/proposal-temporal/#sec-applyunsignedroundingmode */
export function ApplyUnsignedRoundingMode(
  x: number,
  r1: number,
  r2: number,
  unsignedRoundingMode?: UnsignedRoundingMode,
): number {
  if (x === r1) {
    return r1;
  }
  Assert(r1 < x && x < r2);
  Assert(unsignedRoundingMode !== undefined);
  if (unsignedRoundingMode === UnsignedRoundingMode.Zero) {
    return r1;
  }
  if (unsignedRoundingMode === UnsignedRoundingMode.Infinity) {
    return r2;
  }
  const d1 = x - r1;
  const d2 = r2 - x;
  if (d1 < d2) {
    return r1;
  }
  if (d2 < d1) {
    return r2;
  }
  Assert(d1 === d2);
  if (unsignedRoundingMode === UnsignedRoundingMode.HalfZero) {
    return r1;
  }
  if (unsignedRoundingMode === UnsignedRoundingMode.HalfInfinity) {
    return r2;
  }
  Assert(unsignedRoundingMode === UnsignedRoundingMode.HalfEven);
  const cardinality = (r1 / (r2 - r1)) % 2;
  if (cardinality === 0) {
    return r1;
  }
  return r2;
}

/** https://tc39.es/proposal-temporal/#sec-roundnumbertoincrement */
export function RoundNumberToIncrement(
  x: number,
  increment: number,
  roundingMode: RoundingMode,
): number {
  let quotient = x / increment;
  let isNegative: 'negative' | 'positive';
  if (quotient < 0) {
    isNegative = 'negative';
    quotient = -quotient;
  } else {
    isNegative = 'positive';
  }
  const unsignedRoundingMode = GetUnsignedRoundingMode(roundingMode, isNegative);
  // Let r1 be the largest integer such that r1 ≤ quotient.
  const r1 = Math.floor(quotient);
  // Let r2 be the smallest integer such that r2 > quotient.
  const r2 = r1 + 1;
  let rounded = ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode);
  if (isNegative === 'negative') {
    rounded = -rounded;
  }
  return rounded * increment;
}

/** https://tc39.es/proposal-temporal/#sec-roundnumbertoincrementasifpositive */
export function RoundNumberToIncrementAsIfPositive(
  x: number,
  increment: number,
  roundingMode: RoundingMode,
): number {
  const quotient = x / increment;
  const unsignedRoundingMode = GetUnsignedRoundingMode(roundingMode, 'positive');
  // Let r1 be the largest integer such that r1 ≤ quotient.
  const r1 = Math.floor(quotient);
  // Let r2 be the smallest integer such that r2 > quotient.
  const r2 = r1 + 1;
  const rounded = ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode);
  return rounded * increment;
}

/** https://tc39.es/proposal-temporal/#sec-temporal-topositiveintegerwithtruncation */
export function* ToPositiveIntegerWithTruncation(argument: Value): PlainEvaluator<number> {
  const integer = Q(yield* ToIntegerWithTruncation(argument));
  if (integer <= 0) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', integer);
  }
  return integer;
}

// TODO: Review
/** https://tc39.es/proposal-temporal/#sec-temporal-tointegerwithtruncation */
export function* ToIntegerWithTruncation(argument: Value): PlainEvaluator<number> {
  const number = R(Q(yield* ToNumber(argument)));
  if (Number.isNaN(number) || number === Infinity || number === -Infinity) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', number);
  }
  return Math.trunc(number);
}

// TODO: Review
/** https://tc39.es/proposal-temporal/#sec-temporal-tomonthcode */
export function* ToMonthCode(argument: Value): PlainEvaluator<string> {
  const monthCode = Q(yield* ToPrimitive(argument, 'string'));
  if (!(monthCode instanceof JSStringValue)) {
    return surroundingAgent.Throw('TypeError', 'NotAString', monthCode);
  }
  const s = monthCode.stringValue();
  if (s.length !== 3 && s.length !== 4) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
  }
  if (s.charCodeAt(0) !== 0x004D) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
  }
  if (s.charCodeAt(1) < 0x0030 || s.charCodeAt(1) > 0x0039) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
  }
  if (s.charCodeAt(2) < 0x0030 || s.charCodeAt(2) > 0x0039) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
  }
  if (s.length === 4 && s.charCodeAt(3) !== 0x004C) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
  }
  const monthCodeDigits = s.slice(1, 3);
  const monthCodeInteger = Number(monthCodeDigits);
  if (monthCodeInteger === 0 && s.length !== 4) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
  }
  return s;
}

/** https://tc39.es/proposal-temporal/#sec-temporal-tooffsetstring */
export function* ToOffsetString(argument: Value): PlainEvaluator<string> {
  const offset = Q(yield* ToPrimitive(argument, 'string'));
  if (!(offset instanceof JSStringValue)) {
    return surroundingAgent.Throw('TypeError', 'NotAString', offset);
  }
  Q(ParseDateTimeUTCOffset(offset.stringValue()));
  return offset.stringValue();
}

/** https://tc39.es/proposal-temporal/#sec-temporal-isodatetofields */
export function ISODateToFields(
  calendar: CalendarType,
  isoDate: ISODateRecord,
  type: 'date' | 'year-month' | 'month-day',
): CalendarFieldsRecord {
  const fields: CalendarFieldsRecord = {
    Day: undefined,
    Era: undefined,
    EraYear: undefined,
    Hour: undefined,
    Microsecond: undefined,
    Millisecond: undefined,
    Minute: undefined,
    Month: undefined,
    MonthCode: undefined,
    Nanosecond: undefined,
    OffsetString: undefined,
    Second: undefined,
    TimeZone: undefined,
    Year: undefined,
  };
  const calendarDate = CalendarISOToDate(calendar, isoDate);
  fields.MonthCode = calendarDate.MonthCode;
  if (type === 'month-day' || type === 'date') {
    fields.Day = calendarDate.Day;
  }
  if (type === 'year-month' || type === 'date') {
    fields.Year = calendarDate.Year;
  }
  return fields;
}

/** https://tc39.es/proposal-temporal/#sec-temporal-getdifferencesettings */
export function* GetDifferenceSettings(
  operation: 'since' | 'until',
  options: ObjectValue,
  unitGroup: 'date' | 'time' | 'datetime',
  disallowedUnits: readonly TemporalUnit[],
  fallbackSmallestUnit: TemporalUnit,
  smallestLargestDefaultUnit: TemporalUnit,
): PlainEvaluator<{
  SmallestUnit: TemporalUnit,
  LargestUnit: TemporalUnit,
  RoundingMode: RoundingMode,
  RoundingIncrement: number
}> {
  let largestUnit = Q(yield* GetTemporalUnitValuedOption(options, 'largestUnit', 'unset'));
  const roundingIncrement = Q(yield* GetRoundingIncrementOption(options));
  let roundingMode = Q(yield* GetRoundingModeOption(options, RoundingMode.Trunc));
  let smallestUnit = Q(yield* GetTemporalUnitValuedOption(options, 'smallestUnit', 'unset'));
  Q(ValidateTemporalUnitValue(smallestUnit, unitGroup, ['auto']));
  if (largestUnit === 'unset') {
    largestUnit = 'auto';
  }
  if (disallowedUnits.includes(largestUnit as TemporalUnit)) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', largestUnit);
  }
  Q(ValidateTemporalUnitValue(smallestUnit, unitGroup));
  if (smallestUnit === 'unset') {
    smallestUnit = fallbackSmallestUnit;
  }
  if (disallowedUnits.includes(smallestUnit as TemporalUnit)) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', smallestUnit);
  }
  const defaultLargestUnit = LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit as TemporalUnit);
  if (largestUnit === 'auto') {
    largestUnit = defaultLargestUnit;
  }
  if (LargerOfTwoTemporalUnits(largestUnit, smallestUnit as TemporalUnit) !== largestUnit) {
    return surroundingAgent.Throw('RangeError', 'OutOfRange', largestUnit);
  }
  const maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit as TemporalUnit);
  if (maximum !== 'unset') {
    Q(ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false));
  }
  if (operation === 'since') {
    roundingMode = NegateRoundingMode(roundingMode);
  }
  return {
    SmallestUnit: smallestUnit as TemporalUnit,
    LargestUnit: largestUnit,
    RoundingMode: roundingMode,
    RoundingIncrement: roundingIncrement,
  };
}
