export const PARSE_DATETIME =
  /^(\d{4})-(\d{1,2})(-(\d{1,2}))?([^\d]+(\d{1,2}))?(:(\d{1,2}))?(:(\d{1,2}))?(.(\d{1,3}))?$/
export const PARSE_DATE = /^(\d{4})-(\d{1,2})(-(\d{1,2}))?/
export const PARSE_TIME = /(\d\d?)(:(\d\d?)|)(:(\d\d?)|)/

export const DAYS_IN_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
export const DAYS_IN_MONTH_LEAP = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

export const TIME_CONSTANTS = {
  MILLISECONDS_IN: {
    SECOND: 1000,
    MINUTE: 60000,
    HOUR: 3600000,
    DAY: 86400000,
    WEEK: 604800000,
  },
  SECONDS_IN: {
    MINUTE: 60,
    HOUR: 3600,
    DAY: 86400,
    WEEK: 604800,
  },
  MINUTES_IN: {
    MINUTE: 1,
    HOUR: 60,
    DAY: 1440,
    WEEK: 10080,
  },
  HOURS_IN: {
    DAY: 24,
    WEEK: 168,
  },
  DAYS_IN: {
    WEEK: 7,
  },
}

export const DAYS_IN_MONTH_MIN = 28
export const DAYS_IN_MONTH_MAX = 31
export const MONTH_MAX = 12
export const MONTH_MIN = 1
export const DAY_MIN = 1
export const FIRST_HOUR = 0

/**
 * @typedef {Object} Timestamp The Timestamp object
 * @property {string=} Timestamp.date Date string in format 'YYYY-MM-DD'
 * @property {string=} Timestamp.time Time string in format 'HH:MM'
 * @property {number} Timestamp.year The numeric year
 * @property {number} Timestamp.month The numeric month (Jan = 1, ...)
 * @property {number} Timestamp.day The numeric day
 * @property {number} Timestamp.weekday The numeric weekday (Sun = 0, ..., Sat = 6)
 * @property {number=} Timestamp.hour The numeric hour
 * @property {number} Timestamp.minute The numeric minute
 * @property {number=} Timestamp.doy The numeric day of the year (doy)
 * @property {number=} Timestamp.workweek The numeric workweek
 * @property {boolean} Timestamp.hasDay True if Timestamp.date is filled in and usable
 * @property {boolean} Timestamp.hasTime True if Timestamp.time is filled in and usable
 * @property {boolean=} Timestamp.past True if the Timestamp is in the past
 * @property {boolean=} Timestamp.current True if Timestamp is current day (now)
 * @property {boolean=} Timestamp.future True if Timestamp is in the future
 * @property {boolean=} Timestamp.disabled True if this is a disabled date
 * @property {boolean=} Timestamp.currentWeekday True if this date corresponds to current weekday
 */
// export const Timestamp = {
//   date: '', // YYYY-MM-DD
//   time: '', // HH:MM (optional)
//   year: 0, // YYYY
//   month: 0, // MM (Jan = 1, etc)
//   day: 0, // day of the month
//   weekday: 0, // week day (0=Sunday...6=Saturday)
//   hour: 0, // 24-hr format
//   minute: 0, // mm
//   doy: 0, // day of year
//   workweek: 0, // workweek number
//   hasDay: false, // if this timestamp is supposed to have a date
//   hasTime: false, // if this timestamp is supposed to have a time
//   past: false, // if timestamp is in the past (based on `now` property)
//   current: false, // if timestamp is current date (based on `now` property)
//   future: false, // if timestamp is in the future (based on `now` property)
//   disabled: false, // if timestamp is disabled
//   currentWeekday: false, // if this date corresponds to current weekday
// }

// export const TimeObject = {
//   hour: 0, // Number
//   minute: 0, // Number
// }

export interface Timestamp {
  date: string // YYYY-MM-DD
  hasDay: boolean // if this timestamp is supposed to have a date
  year: number // YYYY
  month: number // MM (Jan = 1, etc)
  day: number // day of the month
  time?: string // HH:MM (optional)
  hasTime: boolean // if this timestamp is supposed to have a time
  hour: number // 24-hr format
  minute: number // mm
  weekday?: number // week day (0=Sunday...6=Saturday)
  doy?: number // day of year
  workweek?: number // workweek number
  past?: boolean // if timestamp is in the past (based on `now` property)
  current?: boolean // if timestamp is current date (based on `now` property)
  future?: boolean // if timestamp is in the future (based on `now` property)
  disabled?: boolean // if timestamp is disabled
  currentWeekday?: boolean // if this date corresponds to current weekday
}

export interface TimeObject {
  hour: number // Number
  minute: number // Number
}

/**
 * Validates the passed input ('YYY-MM-DD') as a date or ('YYY-MM-DD HH:MM') date time combination
 * @param {string} input A string in the form 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM'
 * @returns {boolean} True if parseable
 */
export function validateTimestamp(input: string): boolean {
  if (typeof input !== 'string') return false
  return PARSE_DATETIME.test(input)
}

/**
 * Fast low-level parser for a date string ('YYYY-MM-DD'). Does not update formatted or relative date.
 * Use 'parseTimestamp' for formatted and relative updates
 * @param {string} input In the form 'YYYY-MM-DD hh:mm:ss' (seconds are optional, but not used)
 * @returns {Timestamp} This {@link Timestamp} is minimally filled in. The {@link Timestamp.date} and {@link Timestamp.time} as well as relative data will not be filled in.
 */
export function parsed(input: string): Timestamp | null {
  if (typeof input !== 'string') return null
  const parts = PARSE_DATETIME.exec(input)

  if (!parts || !parts[1] || !parts[2]) return null

  const year = parseInt(parts[1], 10)
  const month = parseInt(parts[2], 10)
  const day = parseInt(parts[4] || '1', 10)
  const hour = parseInt(parts[6] || '0', 10)
  const minute = parseInt(parts[8] || '0', 10)

  return {
    date: input,
    time: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`,
    year,
    month,
    day,
    hour,
    minute,
    hasDay: !!parts[4],
    hasTime: true, // time is always present, even if '00:00'
    past: false,
    current: false,
    future: false,
    disabled: false,
    weekday: 0,
    doy: 0,
    workweek: 0,
  }
}

/**
 * Takes a JavaScript Date and returns a {@link Timestamp}. The {@link Timestamp} is not updated with relative information.
 * @param {Date} date JavaScript Date
 * @param {boolean} utc If set the {@link Timestamp} will parse the Date as UTC
 * @returns {Timestamp} A minimal {@link Timestamp} without updated or relative updates.
 */
export function parseDate(date: Date, utc = false): Timestamp | null {
  if (!(date instanceof Date)) return null
  const UTC = utc ? 'UTC' : ''
  return updateFormatted({
    date:
      padNumber(date[`get${UTC}FullYear`](), 4) +
      '-' +
      padNumber(date[`get${UTC}Month`]() + 1, 2) +
      '-' +
      padNumber(date[`get${UTC}Date`](), 2),
    time:
      padNumber(date[`get${UTC}Hours`]() || 0, 2) +
      ':' +
      padNumber(date[`get${UTC}Minutes`]() || 0, 2),
    year: date[`get${UTC}FullYear`](),
    month: date[`get${UTC}Month`]() + 1,
    day: date[`get${UTC}Date`](),
    hour: date[`get${UTC}Hours`](),
    minute: date[`get${UTC}Minutes`](),
    weekday: 0,
    doy: 0,
    workweek: 0,
    hasDay: true,
    hasTime: true, // Date always has time, even if it is '00:00'
    past: false,
    current: false,
    future: false,
    disabled: false,
  } as Timestamp)
}

/**
 * Padds a passed in number to length (converts to a string). Good for converting '5' as '05'.
 * @param {number} x The number to pad
 * @param {number} length The length of the required number as a string
 * @returns {string} The padded number (as a string). (ie: 5 = '05')
 */
export function padNumber(x: number, length: number): string {
  let padded = String(x)
  while (padded.length < length) {
    padded = '0' + padded
  }

  return padded
}

/**
 * Returns if the passed year is a leap year
 * @param {number} year The year to check (ie: 1999, 2020)
 * @returns {boolean} True if the year is a leap year
 */
export function isLeapYear(year: number): boolean {
  // A year is a Gregorian leap year if it is divisible by 4,
  // but not by 100, unless it is also divisible by 400.
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}

/**
 * Returns the days of the specified month in a year
 * @param {number} year The year (ie: 1999, 2020)
 * @param {number} month The month (zero-based)
 * @returns {number} The number of days in the month (corrected for leap years)
 */
export function daysInMonth(year: number, month: number): number {
  return (isLeapYear(year) ? DAYS_IN_MONTH_LEAP[month] : DAYS_IN_MONTH[month]) as number
}

/**
 * Returns a {@link Timestamp} of next day from passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {Timestamp} The modified {@link Timestamp} as the next day
 */
export function nextDay(timestamp: Timestamp): Timestamp {
  const date = new Date(timestamp.year, timestamp.month - 1, timestamp.day + 1)
  return updateFormatted(
    normalizeTimestamp({
      ...timestamp,
      year: date.getFullYear(),
      month: date.getMonth() + 1,
      day: date.getDate(),
    }),
  )
}

/**
 * Returns a {@link Timestamp} of previous day from passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {Timestamp} The modified {@link Timestamp} as the previous day
 */
export function prevDay(timestamp: Timestamp): Timestamp {
  const date = new Date(timestamp.year, timestamp.month - 1, timestamp.day - 1)
  return updateFormatted(
    normalizeTimestamp({
      ...timestamp,
      year: date.getFullYear(),
      month: date.getMonth() + 1,
      day: date.getDate(),
    }),
  )
}

/**
 * Returns today's date
 * @returns {string} Date string in the form 'YYYY-MM-dd'
 */
export function today(): string {
  const d = new Date(),
    month = d.getMonth() + 1,
    day = d.getDate(),
    year = d.getFullYear()

  return [year, padNumber(month, 2), padNumber(day, 2)].join('-')
}

/**
 * Takes a date string ('YYYY-MM-DD') and validates if it is today's date
 * @param {string} date Date string in the form 'YYYY-MM-DD'
 * @returns {boolean} True if the date is today's date
 */
export function isToday(date: string): boolean {
  return date === today()
}

/**
 * Returns the start of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the start of the week).
 * If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}.
 * @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the week
 * @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday
 * @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information
 * @returns {Timestamp} The {@link Timestamp} representing the start of the week
 */
export function getStartOfWeek(
  timestamp: Timestamp,
  weekdays: number[],
  today: Timestamp,
): Timestamp {
  let start = copyTimestamp(timestamp)
  if (!weekdays) {
    return start
  }
  if (start.day === 1 || start.weekday === 0) {
    while (!weekdays.includes(Number(start.weekday))) {
      start = nextDay(start)
    }
  }
  start = findWeekday(start, weekdays[0], prevDay)
  start = updateFormatted(start)
  if (today) {
    start = updateRelative(start, today, start.hasTime)
  }
  return start
}

/**
 * Returns the end of the week give a {@link Timestamp} and weekdays (in which it finds the day representing the last of the week).
 * If today {@link Timestamp} is passed in then this is used to update relative information in the returned {@link Timestamp}.
 * @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the week
 * @param {number[]} weekdays The array is [0,1,2,3,4,5,6] where 0=Sunday and 6=Saturday
 * @param {Timestamp=} today If passed in then the {@link Timestamp} is updated with relative information
 * @returns {Timestamp} The {@link Timestamp} representing the end of the week
 */
export function getEndOfWeek(
  timestamp: Timestamp,
  weekdays: number[],
  today: Timestamp,
): Timestamp {
  let end = copyTimestamp(timestamp)
  if (!weekdays || !Array.isArray(weekdays)) {
    return end
  }

  // is last day of month?
  const lastDay = daysInMonth(end.year, end.month)
  if (lastDay === end.day || end.weekday === weekdays[weekdays.length - 1]) {
    while (!weekdays.includes(Number(end.weekday))) {
      end = prevDay(end)
    }
  }
  end = findWeekday(end, weekdays[weekdays.length - 1]!, nextDay)
  end = updateFormatted(end)
  if (today) {
    end = updateRelative(end, today, end.hasTime)
  }
  return end
}

/**
 * Finds the start of the month based on the passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use to find the start of the month
 * @returns {Timestamp} A {@link Timestamp} of the start of the month
 */
export function getStartOfMonth(timestamp: Timestamp): Timestamp {
  let start = copyTimestamp(timestamp)
  start.day = DAY_MIN
  start = updateFormatted(start)
  return start
}

/**
 * Finds the end of the month based on the passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use to find the end of the month
 * @returns {Timestamp} A {@link Timestamp} of the end of the month
 */
export function getEndOfMonth(timestamp: Timestamp): Timestamp {
  let end = copyTimestamp(timestamp)
  end.day = daysInMonth(end.year, end.month)
  end = updateFormatted(end)
  return end
}

// returns minutes since midnight
export function parseTime(
  input: number | string | { hour: number; minute: number },
): number | false {
  const type = Object.prototype.toString.call(input)
  switch (type) {
    case '[object Number]':
      // when a number is given, it's minutes since 12:00am
      return input as number
    case '[object String]': {
      // when a string is given, it's a hh:mm:ss format where seconds are optional, but not used
      const parts = PARSE_TIME.exec(input as string)
      if (!parts) {
        return false
      }
      return parseInt(parts[1]!, 10) * 60 + parseInt(parts[3] || '0', 10)
    }
    case '[object Object]':
      // when an object is given, it must have hour and minute
      if (
        typeof input !== 'object' ||
        typeof input.hour !== 'number' ||
        typeof input.minute !== 'number'
      ) {
        return false
      }
      if (typeof input === 'object' && 'hour' in input && 'minute' in input) {
        return input.hour * 60 + input.minute
      }
      return false
  }

  return false
}

/**
 * Compares two {@link Timestamp}s for exactness
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 * @returns {boolean} True if the two {@link Timestamp}s are an exact match
 */
export function compareTimestamps(ts1: Timestamp, ts2: Timestamp): boolean {
  if (!ts1 || !ts2) return false

  return (
    ts1.year === ts2.year &&
    ts1.month === ts2.month &&
    ts1.day === ts2.day &&
    ts1.hour === ts2.hour &&
    ts1.minute === ts2.minute
  )
}

/**
 * Compares the date of two {@link Timestamp}s that have been updated with relative data
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 * @returns {boolean} True if the two dates are the same
 */
export function compareDate(ts1: Timestamp, ts2: Timestamp): boolean {
  return getDate(ts1) === getDate(ts2)
}

/**
 * Compares the time of two {@link Timestamp}s that have been updated with relative data
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 * @returns {boolean} True if the two times are an exact match
 */
export function compareTime(ts1: Timestamp, ts2: Timestamp): boolean {
  return getTime(ts1) === getTime(ts2)
}

/**
 * Compares the date and time of two {@link Timestamp}s that have been updated with relative data
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 * @returns {boolean} True if the date and time are an exact match
 */
export function compareDateTime(ts1: Timestamp, ts2: Timestamp): boolean {
  return getDateTime(ts1) === getDateTime(ts2)
}

/**
 * High-level parser that converts the passed in string to {@link Timestamp} and uses 'now' to update relative information.
 * @param {string} input In the form 'YYYY-MM-DD hh:mm:ss' (seconds are optional, but not used)
 * @param {Timestamp} now A {@link Timestamp} to use for relative data updates
 * @returns {Timestamp} The {@link Timestamp.date} will be filled in as well as the {@link Timestamp.time} if a time is supplied and formatted fields (doy, weekday, workweek, etc). If 'now' is supplied, then relative data will also be updated.
 */
export function parseTimestamp(input: string, now: Timestamp | null = null): Timestamp | null {
  let timestamp = parsed(input)
  if (!timestamp) return null

  timestamp = updateFormatted(timestamp as Timestamp)

  if (now) {
    timestamp = updateRelative(timestamp as Timestamp, now, timestamp.hasTime)
  }

  return timestamp as Timestamp
}

/**
 * Converts a {@link Timestamp} into a numeric date identifier based on the passed {@link Timestamp}'s date
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {number} The numeric date identifier
 */
export function getDayIdentifier(timestamp: Timestamp): number {
  return (
    (timestamp.year ?? 0) * 100000000 +
    (timestamp.month ?? 0) * 1000000 +
    (timestamp.day ?? 0) * 10000
  )
}

/**
 * Converts a {@link Timestamp} into a numeric time identifier based on the passed {@link Timestamp}'s time
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {number} The numeric time identifier
 */
export function getTimeIdentifier(timestamp: Timestamp): number {
  return (timestamp.hour ?? 0) * 100 + (timestamp.minute ?? 0)
}

/**
 * Converts a {@link Timestamp} into a numeric date and time identifier based on the passed {@link Timestamp}'s date and time
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {number} The numeric date+time identifier
 */
export function getDayTimeIdentifier(timestamp: Timestamp): number {
  return getDayIdentifier(timestamp) + getTimeIdentifier(timestamp)
}

/**
 * Returns the difference between two {@link Timestamp}s
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 * @param {boolean=} strict Optional flag to not to return negative numbers
 * @returns {number} The difference
 */
export function diffTimestamp(ts1: Timestamp, ts2: Timestamp, strict = false): number {
  const utc1 = Date.UTC(
    ts1.year ?? 0,
    (ts1.month ?? 1) - 1,
    ts1.day ?? 1,
    ts1.hour ?? 0,
    ts1.minute ?? 0,
  )
  const utc2 = Date.UTC(
    ts2.year ?? 0,
    (ts2.month ?? 1) - 1,
    ts2.day ?? 1,
    ts2.hour ?? 0,
    ts2.minute ?? 0,
  )
  if (strict === true && utc2 < utc1) {
    // Not negative number
    // utc2 - utc1 < 0  -> utc2 < utc1 ->   NO: utc1 >= utc2
    return 0
  }
  return utc2 - utc1
}

/**
 * Updates a {@link Timestamp} with relative data (past, current and future)
 * @param {Timestamp} timestamp The {@link Timestamp} that needs relative data updated
 * @param {Timestamp} now {@link Timestamp} that represents the current date (optional time)
 * @param {boolean=} time Optional flag to include time ('timestamp' and 'now' params should have time values)
 * @returns {Timestamp} A new {@link Timestamp}
 */
export function updateRelative(timestamp: Timestamp, now: Timestamp, time = false): Timestamp {
  let ts = copyTimestamp(timestamp as Timestamp)
  let a = getDayIdentifier(now)
  let b = getDayIdentifier(ts)
  let current = a === b

  if (ts.hasTime && time && current) {
    a = getTimeIdentifier(now)
    b = getTimeIdentifier(ts)
    current = a === b
  }

  ts.past = b < a
  ts.current = current
  ts.future = b > a
  ts.currentWeekday = ts.weekday === now.weekday

  return ts
}

/**
 * Sets a Timestamp{@link Timestamp} to number of minutes past midnight (modifies hour and minutes if needed)
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @param {number} minutes The number of minutes to set from midnight
 * @param {Timestamp=} now Optional {@link Timestamp} representing current date and time
 * @returns {Timestamp} A new {@link Timestamp}
 */
export function updateMinutes(
  timestamp: Timestamp,
  minutes: number,
  now: Timestamp | null = null,
): Timestamp {
  let ts = copyTimestamp(timestamp)
  ts.hasTime = true
  ts.hour = Math.floor(minutes / TIME_CONSTANTS.MINUTES_IN.HOUR)
  ts.minute = minutes % TIME_CONSTANTS.MINUTES_IN.HOUR
  ts.time = getTime(ts)
  if (now) {
    ts = updateRelative(ts, now, true)
  }

  return ts
}

/**
 * Updates the {@link Timestamp} with the weekday
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @returns A new Timestamp
 */
export function updateWeekday(timestamp: Timestamp): Timestamp {
  let ts = copyTimestamp(timestamp)
  ts.weekday = getWeekday(ts)

  return ts
}

/**
 * Updates the {@link Timestamp} with the day of the year (doy)
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @returns A new Timestamp
 */
export function updateDayOfYear(timestamp: Timestamp): Timestamp {
  let ts = copyTimestamp(timestamp)
  ts.doy = getDayOfYear(ts) || 0

  return ts
}

/**
 * Updates the {@link Timestamp} with the workweek
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @returns A new {@link Timestamp}
 */
export function updateWorkWeek(timestamp: Timestamp): Timestamp {
  let ts = copyTimestamp(timestamp)
  ts.workweek = getWorkWeek(ts)

  return ts
}

/**
 * Updates the passed {@link Timestamp} with disabled, if needed
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @param {string} [disabledBefore] In 'YYY-MM-DD' format
 * @param {string} [disabledAfter] In 'YYY-MM-DD' format
 * @param {number[]} [disabledWeekdays] An array of numbers representing weekdays [0 = Sun, ..., 6 = Sat]
 * @param {string[]|string[][]} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range.
 * @returns A new {@link Timestamp}
 */
export function updateDisabled(
  timestamp: Timestamp,
  disabledBefore?: string,
  disabledAfter?: string,
  disabledWeekdays?: number[],
  disabledDays?: string[] | string[][],
): Timestamp {
  let ts = copyTimestamp(timestamp)
  const t = getDayIdentifier(ts)

  if (disabledBefore !== undefined) {
    const disabledDay = parsed(disabledBefore)
    if (disabledDay) {
      const before = getDayIdentifier(disabledDay)
      if (t <= before) {
        ts.disabled = true
      }
    }
  }

  if (ts.disabled !== true && disabledAfter !== undefined) {
    const disabledDay = parsed(disabledAfter!)
    if (disabledDay) {
      const after = getDayIdentifier(disabledDay)
      if (t >= after) {
        ts.disabled = true
      }
    }
  }

  if (ts.disabled !== true && Array.isArray(disabledWeekdays) && disabledWeekdays.length > 0) {
    for (const weekday in disabledWeekdays) {
      if (disabledWeekdays[weekday] === ts.weekday) {
        ts.disabled = true
        break
      }
    }
  }

  if (ts.disabled !== true && Array.isArray(disabledDays) && disabledDays.length > 0) {
    for (const day in disabledDays) {
      if (
        Array.isArray(disabledDays[day]) &&
        disabledDays[day].length === 2 &&
        disabledDays[day][0] &&
        disabledDays[day][1]
      ) {
        const start = parsed(disabledDays[day][0])
        const end = parsed(disabledDays[day][1])
        if (start && end && isBetweenDates(ts, start, end)) {
          ts.disabled = true
          break
        }
      } else {
        const disabledDayOrRange = disabledDays[day]
        // handle ranges with multiple days
        if (Array.isArray(disabledDayOrRange)) {
          for (const range of disabledDayOrRange) {
            const disabledDay = parseTimestamp(range)
            if (disabledDay) {
              const d = getDayIdentifier(disabledDay)
              if (d === t) {
                ts.disabled = true
                break
              }
            }
          }
        } else if (disabledDayOrRange) {
          const disabledDay = parseTimestamp(disabledDayOrRange)
          if (disabledDay) {
            const d = getDayIdentifier(disabledDay)
            if (d === t) {
              ts.disabled = true
            }
          }
        }
      }
    }
  }

  return ts
}

/**
 * Updates the passed {@link Timestamp} with formatted data (time string, date string, weekday, day of year and workweek)
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @returns A new {@link Timestamp}
 */
export function updateFormatted(timestamp: Timestamp): Timestamp {
  let ts = copyTimestamp(timestamp)
  ts.hasTime = true
  ts.time = getTime(ts)
  ts.date = getDate(ts)
  ts.weekday = getWeekday(ts)
  ts.doy = getDayOfYear(ts) || 0
  ts.workweek = getWorkWeek(ts)

  return ts
}

/**
 * Returns day of the year (doy) for the passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {number} The day of the year
 */
export function getDayOfYear(timestamp: Timestamp): number | void {
  if (timestamp.year === 0) return
  return (
    (Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day) -
      Date.UTC(timestamp.year, 0, 0)) /
    24 /
    60 /
    60 /
    1000
  )
}

/**
 * Returns workweek for the passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {number} The work week
 */
export function getWorkWeek(timestamp: Timestamp): number {
  let ts = copyTimestamp(timestamp)
  if (ts.year === 0) {
    const parsedToday = parseTimestamp(today())
    if (parsedToday) {
      ts = parsedToday as Timestamp
    }
  }

  // Remove time components of date
  const weekday = new Date(Date.UTC(ts.year, ts.month - 1, ts.day))

  // Adjust the date to the correct day of the week
  const dayAdjustment = 4 // thursday is 4
  weekday.setUTCDate(weekday.getUTCDate() - ((weekday.getUTCDay() + 6) % 7) + dayAdjustment)

  // Set to nearest Thursday: current date + 4 - current day number
  // Make Sunday's day number 7
  weekday.setUTCDate(weekday.getUTCDate() + dayAdjustment - (weekday.getUTCDay() || 7))

  // Get first day of year
  var yearStart = new Date(Date.UTC(weekday.getUTCFullYear(), 0, 1))

  // Calculate full weeks to nearest Thursday
  var weekNumber = Math.ceil(((weekday.valueOf() - yearStart.valueOf()) / 86400000 + 1) / 7)

  return weekNumber
}

/**
 * Returns weekday for the passed in {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @returns {number} The weekday
 */
export function getWeekday(timestamp: Timestamp): number {
  let weekday = timestamp.weekday
  if (timestamp.hasDay) {
    const floor = Math.floor
    const day = timestamp.day
    const month = ((timestamp.month + 9) % MONTH_MAX) + 1
    const century = floor(timestamp.year / 100)
    const year = (timestamp.year % 100) - (timestamp.month <= 2 ? 1 : 0)

    weekday =
      (((day +
        floor(2.6 * month - 0.2) -
        2 * century +
        year +
        floor(year / 4) +
        floor(century / 4)) %
        7) +
        7) %
      7
  }

  return weekday ?? 0
}

/**
 * Makes a copy of the passed in {@link Timestamp}
 * @param {Timestamp} timestamp The original {@link Timestamp}
 * @returns {Timestamp} A copy of the original {@link Timestamp}
 */
export function copyTimestamp(timestamp: Timestamp): Timestamp {
  return { ...timestamp }
}

/**
 * Used internally to convert {@link Timestamp} used with 'parsed' or 'parseDate' so the 'date' portion of the {@link Timestamp} is correct.
 * @param {Timestamp} timestamp The (raw) {@link Timestamp}
 * @returns {string} A formatted date ('YYYY-MM-DD')
 */
export function getDate(timestamp: Timestamp): string {
  let str = `${padNumber(timestamp.year, 4)}-${padNumber(timestamp.month, 2)}`

  if (timestamp.hasDay) str += `-${padNumber(timestamp.day, 2)}`

  return str
}

/**
 * Used intenally to convert {@link Timestamp} with 'parsed' or 'parseDate' so the 'time' portion of the {@link Timestamp} is correct.
 * @param {Timestamp} timestamp The (raw) {@link Timestamp}
 * @returns {string} A formatted time ('hh:mm')
 */
export function getTime(timestamp: Timestamp): string {
  if (!timestamp.hasTime) {
    return ''
  }

  return `${padNumber(timestamp.hour, 2)}:${padNumber(timestamp.minute, 2)}`
}

/**
 * Returns a formatted string date and time ('YYYY-YY-MM hh:mm')
 * @param {Timestamp} timestamp The {@link Timestamp}
 * @returns {string} A formatted date time ('YYYY-MM-DD HH:mm')
 */
export function getDateTime(timestamp: Timestamp): string {
  return getDate(timestamp) + ' ' + (timestamp.hasTime ? getTime(timestamp) : '00:00')
}

/**
 * An alias for {relativeDays}
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}).
 * @param {number} [days=1] The number of days to move.
 * @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat].
 * @returns The modified {@link Timestamp}
 */
export function moveRelativeDays(
  timestamp: Timestamp,
  mover = nextDay,
  days = 1,
  allowedWeekdays = [0, 1, 2, 3, 4, 5, 6],
): Timestamp {
  const ts = copyTimestamp(timestamp)
  return relativeDays(ts, mover, days, allowedWeekdays)
}

/**
 * Moves the {@link Timestamp} the number of relative days
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @param {function} [mover=nextDay] The mover function to use (ie: {nextDay} or {prevDay}).
 * @param {number} [days=1] The number of days to move.
 * @param {number[]} [allowedWeekdays=[ 0, 1, 2, 3, 4, 5, 6 ]] An array of numbers representing the weekdays. ie: [0 = Sun, ..., 6 = Sat].
 * @returns A new {@link Timestamp}
 */
export function relativeDays(
  timestamp: Timestamp,
  mover = nextDay,
  days = 1,
  allowedWeekdays = [0, 1, 2, 3, 4, 5, 6],
): Timestamp {
  let ts = copyTimestamp(timestamp)
  if (!allowedWeekdays.includes(Number(ts.weekday)) && ts.weekday === 0 && mover === nextDay) {
    ++days
  }
  while (--days >= 0) {
    ts = mover(ts)
    if (allowedWeekdays.length < 7 && !allowedWeekdays.includes(Number(ts.weekday))) {
      ++days
    }
  }

  return ts
}

/**
 * Finds the specified weekday (forward or back) based on the {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to modify
 * @param {number} weekday The weekday number (Sun = 0, ..., Sat = 6)
 * @param {function} [mover=nextDay] The function to use ({prevDay} or {nextDay}).
 * @param {number} [maxDays=6] The number of days to look forward or back.
 * @returns A new {@link Timestamp}
 */
export function findWeekday(
  timestamp: Timestamp,
  weekday: number,
  mover = nextDay,
  maxDays = 6,
): Timestamp {
  let ts = copyTimestamp(timestamp)
  while (ts.weekday !== weekday && --maxDays >= 0) ts = mover(ts)
  return ts
}

/**
 * Creates an array of {@link Timestamp}s based on start and end params
 * @param {Timestamp} start The starting {@link Timestamp}
 * @param {Timestamp} end The ending {@link Timestamp}
 * @param {Timestamp} now The relative day
 * @param {number[]} weekdays An array of numbers (representing days of the week) that are 0 (=Sunday) to 6 (=Saturday)
 * @param {string} [disabledBefore] Days before this date are disabled (YYYY-MM-DD)
 * @param {string} [disabledAfter] Days after this date are disabled (YYYY-MM-DD)
 * @param {number[]} [disabledWeekdays] An array representing weekdays that are disabled [0 = Sun, ..., 6 = Sat]
 * @param {string[]} [disabledDays] An array of days in 'YYYY-MM-DD' format. If an array with a pair of dates is in first array, then this is treated as a range.
 * @param {number} [max=42] Max days to do
 * @param {number} [min=0]  Min days to do
 * @returns {Timestamp[]} The requested array of {@link Timestamp}s
 */
export function createDayList(
  start: Timestamp,
  end: Timestamp,
  now: Timestamp,
  weekdays: number[] = [0, 1, 2, 3, 4, 5, 6],
  disabledBefore: string | undefined = undefined,
  disabledAfter: string | undefined = undefined,
  disabledWeekdays: number[] = [],
  disabledDays: string[] = [],
  max = 42,
  min = 0,
): Timestamp[] {
  const begin = getDayIdentifier(start)
  const stop = getDayIdentifier(end)
  const days: Timestamp[] = []
  let current = copyTimestamp(start)
  let currentIdentifier = 0
  let stopped = currentIdentifier === stop

  if (stop < begin) {
    return days
  }

  while ((!stopped || days.length < min) && days.length < max) {
    currentIdentifier = getDayIdentifier(current)
    stopped = stopped || (currentIdentifier > stop && days.length >= min)
    if (stopped) {
      break
    }
    if (!weekdays.includes(Number(current.weekday))) {
      current = relativeDays(current, nextDay)
      continue
    }
    let day = copyTimestamp(current)
    day = updateFormatted(day)
    day = updateRelative(day, now)
    day = updateDisabled(day, disabledBefore, disabledAfter, disabledWeekdays, disabledDays)
    days.push(day)
    current = relativeDays(current, nextDay)
  }

  return days
}

/**
 * Creates an array of interval {@link Timestamp}s based on params
 * @param {Timestamp} timestamp The starting {@link Timestamp}
 * @param {number} first The starting interval time
 * @param {number} minutes How many minutes between intervals (ie: 60, 30, 15 would be common ones)
 * @param {number} count The number of intervals needed
 * @param {Timestamp} now A relative {@link Timestamp} with time
 * @returns {Timestamp[]} The requested array of interval {@link Timestamp}s
 */
export function createIntervalList(
  timestamp: Timestamp,
  first: number,
  minutes: number,
  count: number,
  now: Timestamp,
): Timestamp[] {
  const intervals = []

  for (let i = 0; i < count; ++i) {
    const mins = (first + i) * minutes
    const ts = copyTimestamp(timestamp)
    intervals.push(updateMinutes(ts, mins, now))
  }

  return intervals
}

export type LocaleFormatter = (_timestamp: Timestamp, _short: boolean) => Intl.DateTimeFormatOptions
export type WeekdayFormatter = (
  _weekday: keyof typeof weekdayDateMap,
  _type: string,
  _locale?: string,
) => string
export type MonthFormatter = (_month: number, _type: string, _locale?: string) => string

/**
 * @callback getOptions
 * @param {Timestamp} timestamp A {@link Timestamp} object
 * @param {boolean} short True if using short options
 * @returns {Object} An Intl object representing optioons to be used
 */

/**
 * @callback formatter
 * @param {Timestamp} timestamp The {@link Timestamp} being used
 * @param {boolean} short If short format is being requested
 * @returns {string} The localized string of the formatted {@link Timestamp}
 */

/**
 * Returns a function that uses Intl.DateTimeFormat formatting
 * @param {string} locale The locale to use (ie: en-US)
 * @param {getOptions} cb The function to call for options. This function should return an Intl formatted object. The function is passed (timestamp, short).
 * @returns {formatter} The function has params (timestamp, short). The short is to use the short options.
 */
export function createNativeLocaleFormatter(
  locale: string,
  cb: LocaleFormatter,
): (_timestamp: Timestamp, _short: boolean) => string {
  const emptyFormatter = (): string => ''

  /* istanbul ignore next */
  if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
    return emptyFormatter
  }

  return (timestamp: Timestamp, short: boolean): string => {
    try {
      const intlFormatter = new Intl.DateTimeFormat(locale || undefined, cb(timestamp, short))
      return intlFormatter.format(makeDateTime(timestamp))
    } catch (e) /* istanbul ignore next */ {
      console.error(`Intl.DateTimeFormat: ${(e as Error).message} -> ${getDateTime(timestamp)}`)
      return ''
    }
  }
}

/**
 * Makes a JavaScript Date from the passed {@link Timestamp}
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @param {boolean} utc True to get Date object using UTC
 * @returns {Date} A JavaScript Date
 */
export function makeDate(timestamp: Timestamp, utc = true): Date {
  if (utc) return new Date(Date.UTC(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0))
  return new Date(timestamp.year, timestamp.month - 1, timestamp.day, 0, 0)
}

/**
 * Makes a JavaScript Date from the passed {@link Timestamp} (with time)
 * @param {Timestamp} timestamp The {@link Timestamp} to use
 * @param {boolean} utc True to get Date object using UTC
 * @returns {Date} A JavaScript Date
 */
export function makeDateTime(timestamp: Timestamp, utc = true): Date {
  if (utc)
    return new Date(
      Date.UTC(
        timestamp.year,
        timestamp.month - 1,
        timestamp.day,
        timestamp.hour,
        timestamp.minute,
      ),
    )
  return new Date(
    timestamp.year,
    timestamp.month - 1,
    timestamp.day,
    timestamp.hour,
    timestamp.minute,
  )
}

/**
 * Validates if the input is a finite number.
 *
 * @param input - The value to be validated. Can be a string or a number.
 * @returns A boolean indicating whether the input is a finite number.
 *          Returns true if the input is a finite number, false otherwise.
 */
export function validateNumber(input: string | number): boolean {
  return isFinite(Number(input))
}

/**
 * Given an array of {@link Timestamp}s, finds the max date (and possible time)
 * @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s
 * @param {boolean=} useTime Default false; if true, uses time in the comparison as well
 * @returns The {@link Timestamp} with the highest date (and possibly time) value
 */
export function maxTimestamp(timestamps: Timestamp[], useTime = false): Timestamp {
  const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier
  return timestamps.reduce((prev, cur) => {
    return Math.max(func(prev), func(cur)) === func(prev) ? prev : cur
  })
}

/**
 * Given an array of {@link Timestamp}s, finds the min date (and possible time)
 * @param {Timestamp[]} timestamps This is an array of {@link Timestamp}s
 * @param {boolean=} useTime Default false; if true, uses time in the comparison as well
 * @returns The {@link Timestamp} with the lowest date (and possibly time) value
 */
export function minTimestamp(timestamps: Timestamp[], useTime = false): Timestamp {
  const func = useTime === true ? getDayTimeIdentifier : getDayIdentifier
  return timestamps.reduce((prev, cur) => {
    return Math.min(func(prev), func(cur)) === func(prev) ? prev : cur
  })
}

/**
 * Determines if the passed {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range)
 * @param {Timestamp} timestamp The {@link Timestamp} for testing
 * @param {Timestamp} startTimestamp The starting {@link Timestamp}
 * @param {Timestamp} endTimestamp The ending {@link Timestamp}
 * @param {boolean=} useTime If true, use time from the {@link Timestamp}s
 * @returns {boolean} True if {@link Timestamp} is between (or equal) to two {@link Timestamp}s (range)
 */
export function isBetweenDates(
  timestamp: Timestamp,
  startTimestamp: Timestamp,
  endTimestamp: Timestamp,
  useTime = false,
): boolean {
  const cd = getDayIdentifier(timestamp) + (useTime === true ? getTimeIdentifier(timestamp) : 0)
  const sd =
    getDayIdentifier(startTimestamp) + (useTime === true ? getTimeIdentifier(startTimestamp) : 0)
  const ed =
    getDayIdentifier(endTimestamp) + (useTime === true ? getTimeIdentifier(endTimestamp) : 0)

  return cd >= sd && cd <= ed
}

/**
 * Determine if two ranges of {@link Timestamp}s overlap each other
 * @param {Timestamp} startTimestamp The starting {@link Timestamp} of first range
 * @param {Timestamp} endTimestamp The endinging {@link Timestamp} of first range
 * @param {Timestamp} firstTimestamp The starting {@link Timestamp} of second range
 * @param {Timestamp} lastTimestamp The ending {@link Timestamp} of second range
 * @returns {boolean} True if the two ranges overlap each other
 */
export function isOverlappingDates(
  startTimestamp: Timestamp,
  endTimestamp: Timestamp,
  firstTimestamp: Timestamp,
  lastTimestamp: Timestamp,
): boolean {
  const start = getDayIdentifier(startTimestamp)
  const end = getDayIdentifier(endTimestamp)
  const first = getDayIdentifier(firstTimestamp)
  const last = getDayIdentifier(lastTimestamp)
  return (
    (start >= first && start <= last) || // overlap left
    (end >= first && end <= last) || // overlap right
    (first >= start && end >= last) // surrounding
  )
}

export interface AddToDateOptions {
  year?: number
  month?: number
  day?: number
  hour?: number
  minute?: number
}

/**
 * Add or decrements years, months, days, hours or minutes to a timestamp
 * @param {Timestamp} timestamp The {@link Timestamp} object
 * @param {Object} options configuration data
 * @param {number=} options.year If positive, adds years. If negative, removes years.
 * @param {number=} options.month If positive, adds months. If negative, removes month.
 * @param {number=} options.day If positive, adds days. If negative, removes days.
 * @param {number=} options.hour If positive, adds hours. If negative, removes hours.
 * @param {number=} options.minute If positive, adds minutes. If negative, removes minutes.
 * @returns {Timestamp} A modified copy of the passed in {@link Timestamp}
 */
export function addToDate(timestamp: Timestamp, options: AddToDateOptions): Timestamp {
  const ts = copyTimestamp(timestamp)

  if (options.year) ts.year += options.year
  if (options.month) ts.month += options.month
  if (options.day) ts.day += options.day
  if (options.hour) ts.hour += options.hour
  if (options.minute) ts.minute += options.minute

  return updateFormatted(normalizeTimestamp(ts))
}

/**
 * Normalizes a timestamp object by creating a JavaScript Date object and extracting standardized values.
 * This function ensures that the timestamp values are consistent and correctly represent a valid date and time.
 *
 * @param {Object} ts - The timestamp object to normalize.
 * @param {number} ts.year - The year of the timestamp.
 * @param {number} ts.month - The month of the timestamp (1-12).
 * @param {number} ts.day - The day of the month.
 * @param {number} ts.hour - The hour of the day (0-23).
 * @param {number} ts.minute - The minute of the hour (0-59).
 * @returns {Object} A new object with normalized timestamp values.
 *                   The returned object includes all properties from the input object,
 *                   with year, month, day, hour, and minute properties updated to normalized values.
 */
function normalizeTimestamp(ts: Timestamp): Timestamp {
  const date = new Date(ts.year, ts.month - 1, ts.day, ts.hour, ts.minute)
  return {
    ...ts,
    year: date.getFullYear(),
    month: date.getMonth() + 1,
    day: date.getDate(),
    hour: date.getHours(),
    minute: date.getMinutes(),
  }
}

/**
 * Returns number of days between two {@link Timestamp}s
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 * @returns Number of days
 */
export function daysBetween(ts1: Timestamp, ts2: Timestamp): number {
  const diff = diffTimestamp(ts1, ts2, true)
  return Math.floor(diff / TIME_CONSTANTS.MILLISECONDS_IN.DAY)
}

/**
 * Returns number of weeks between two {@link Timestamp}s
 * @param {Timestamp} ts1 The first {@link Timestamp}
 * @param {Timestamp} ts2 The second {@link Timestamp}
 */
export function weeksBetween(ts1: Timestamp, ts2: Timestamp): number {
  let t1 = copyTimestamp(ts1)
  let t2 = copyTimestamp(ts2)
  t1 = findWeekday(t1, 0)
  t2 = findWeekday(t2, 6)
  return Math.ceil(daysBetween(t1, t2) / TIME_CONSTANTS.DAYS_IN.WEEK)
}

// Known dates
const weekdayDateMap = {
  Sun: new Date('2020-01-05T00:00:00.000Z'),
  Mon: new Date('2020-01-06T00:00:00.000Z'),
  Tue: new Date('2020-01-07T00:00:00.000Z'),
  Wed: new Date('2020-01-08T00:00:00.000Z'),
  Thu: new Date('2020-01-09T00:00:00.000Z'),
  Fri: new Date('2020-01-10T00:00:00.000Z'),
  Sat: new Date('2020-01-11T00:00:00.000Z'),
}

/**
 * Returns a function that uses Intl.DateTimeFormat to format weekdays.
 *
 * @function getWeekdayFormatter
 * @returns {function} A function that formats weekdays.
 *
 * @example
 * const formatWeekday = getWeekdayFormatter();
 * console.log(formatWeekday('Mon', 'long', 'en-US')); // "Monday"
 * console.log(formatWeekday('Mon', 'short', 'fr-FR')); // "lun."
 *
 * @param {string} weekday - The abbreviation of the weekday (e.g., 'Mon', 'Tue', 'Wed', etc.).
 * @param {string} [type='long'] - The type of formatting to use ('narrow', 'short', or 'long').
 * @param {string} [locale=''] - The locale to use for formatting.
 *
 * @returns {string} The formatted weekday.
 */
export function getWeekdayFormatter(): WeekdayFormatter {
  const emptyFormatter = (): string => ''
  const options = {
    long: { timeZone: 'UTC', weekday: 'long' },
    short: { timeZone: 'UTC', weekday: 'short' },
    narrow: { timeZone: 'UTC', weekday: 'narrow' },
  }

  /* istanbul ignore next */
  if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
    return emptyFormatter as WeekdayFormatter
  }

  /**
   * Formats a given weekday into a localized string based on the specified type and locale.
   *
   * @param {number} weekday - The day of the week (0 for Sunday, 1 for Monday, etc.).
   * @param {string} type - The format type (e.g., 'narrow', 'short', 'long') to use for formatting.
   * @param {string} [locale] - The locale string (e.g., 'en-US') to use for formatting. Defaults to the user's locale if not provided.
   * @returns {string} The formatted weekday string.
   */
  function weekdayFormatter(
    weekday: keyof typeof weekdayDateMap,
    type: string,
    locale?: string,
  ): string {
    try {
      const intlFormatter = new Intl.DateTimeFormat(
        locale || undefined,
        /// @ts-expect-error ignore for now
        options[type] || options['long'],
      )
      return intlFormatter.format(weekdayDateMap[weekday])
    } catch (e) /* istanbul ignore next */ {
      if (e instanceof Error) {
        console.error(`Intl.DateTimeFormat: ${e.message} -> day of week: ${weekday}`)
      }
      return ''
    }
  }

  return weekdayFormatter
}

/**
 * Retrieves an array of localized weekday names.
 *
 * @param {string} type - The format type for the weekday names. Can be 'narrow', 'short', or 'long'.
 * @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
 * @returns {string[]} An array of localized weekday names in the specified format.
 */
export function getWeekdayNames(type: string, locale: string): string[] {
  const shortWeekdays = Object.keys(weekdayDateMap)
  const weekdayFormatter = getWeekdayFormatter()
  return shortWeekdays.map((weekday) =>
    String(weekdayFormatter(weekday as keyof typeof weekdayDateMap, type, locale)),
  )
}

/**
 * Creates and returns a function for formatting month names based on locale and format type.
 *
 * @returns {Function} A function that formats month names.
 *   The returned function accepts the following parameters:
 *   @param {number} month - The month to format (0-11, where 0 is January).
 *   @param {string} [type='long'] - The format type: 'narrow', 'short', or 'long'.
 *   @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
 *   @returns {string} The formatted month name.
 *
 * @throws {Error} If Intl or Intl.DateTimeFormat is not supported in the environment.
 */
export function getMonthFormatter(): MonthFormatter {
  const emptyFormatter = (): string => ''
  const options: Record<'long' | 'short' | 'narrow', { timeZone: string; month: string }> = {
    long: { timeZone: 'UTC', month: 'long' },
    short: { timeZone: 'UTC', month: 'short' },
    narrow: { timeZone: 'UTC', month: 'narrow' },
  }

  /* istanbul ignore next */
  if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat === 'undefined') {
    return emptyFormatter
  }

  /**
   * Formats a given month into a string based on the specified type and locale.
   *
   * @param {number} month - The month to format (0 for January, 11 for December).
   * @param {string} type - The format type (e.g., 'narrow', 'long', 'short', etc.).
   * @param {string} [locale] - The locale to use for formatting (defaults to the system locale if not provided).
   * @returns {string} The formatted month string.
   */
  function monthFormatter(month: number, type: string, locale?: string): string {
    try {
      const intlFormatter = new Intl.DateTimeFormat(
        locale || undefined,
        /// @ts-expect-error ignore for now
        options[type] || options['long'],
      )
      const date = new Date()
      date.setDate(1)
      date.setMonth(month)
      return intlFormatter.format(date)
    } catch (e: unknown) /* istanbul ignore next */ {
      if (e instanceof Error) {
        console.error(`Intl.DateTimeFormat: ${e.message} -> month: ${month}`)
      }
      return ''
    }
  }

  return monthFormatter
}

/**
 * Retrieves an array of localized month names.
 *
 * @param {string} type - The format type for the month names. Can be 'narrow', 'short', or 'long'.
 * @param {string} [locale] - The locale to use for formatting. If not provided, the default locale is used.
 * @returns {string[]} An array of localized month names in the specified format.
 */
export function getMonthNames(type: string, locale: string): string[] {
  const monthFormatter = getMonthFormatter()
  return [...Array(12).keys()].map((month) => monthFormatter(month, type, locale))
}

// the exports...
export default {
  PARSE_DATETIME,
  PARSE_DATE,
  PARSE_TIME,
  DAYS_IN_MONTH,
  DAYS_IN_MONTH_LEAP,
  DAYS_IN_MONTH_MIN,
  DAYS_IN_MONTH_MAX,
  MONTH_MAX,
  MONTH_MIN,
  DAY_MIN,
  TIME_CONSTANTS,
  FIRST_HOUR,
  // Timestamp,
  // TimeObject,
  today,
  getStartOfWeek,
  getEndOfWeek,
  getStartOfMonth,
  getEndOfMonth,
  parseTime,
  validateTimestamp,
  parsed,
  parseTimestamp,
  parseDate,
  getDayIdentifier,
  getTimeIdentifier,
  getDayTimeIdentifier,
  diffTimestamp,
  updateRelative,
  updateMinutes,
  updateWeekday,
  updateDayOfYear,
  updateWorkWeek,
  updateDisabled,
  updateFormatted,
  getDayOfYear,
  getWorkWeek,
  getWeekday,
  isLeapYear,
  daysInMonth,
  copyTimestamp,
  padNumber,
  getDate,
  getTime,
  getDateTime,
  nextDay,
  prevDay,
  relativeDays,
  findWeekday,
  createDayList,
  createIntervalList,
  createNativeLocaleFormatter,
  makeDate,
  makeDateTime,
  validateNumber,
  isBetweenDates,
  isOverlappingDates,
  daysBetween,
  weeksBetween,
  addToDate,
  compareTimestamps,
  compareDate,
  compareTime,
  compareDateTime,
  getWeekdayFormatter,
  getWeekdayNames,
  getMonthFormatter,
  getMonthNames,
}
