text: """
  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.
  """
  
  from __future__ import annotations
  
  import datetime
  import re
  from typing import List, Optional, Tuple
  
  import parsedatetime as pdt
  from dateutil.relativedelta import relativedelta
  from discord.ext import commands
  
  from .context import Context
  from .formats import format_dt, human_join, plural
  
  # Monkey patch mins and secs into the units
  units = pdt.pdtLocales["en_US"].units
  units["minutes"].append("mins")
  units["seconds"].append("secs")
  units["hours"].append("hr")
  units["hours"].append("hrs")
  
  
  class ShortTime:
      compiled = re.compile(
          """(?:(?P<years>[0-9])(?:years?|y))?             # e.g. 2y
                               (?:(?P<months>[0-9]{1,2})(?:months?|mo))?     # e.g. 2months
                               (?:(?P<weeks>[0-9]{1,4})(?:weeks?|w))?        # e.g. 10w
                               (?:(?P<days>[0-9]{1,5})(?:days?|d))?          # e.g. 14d
                               (?:(?P<hours>[0-9]{1,5})(?:hours?|h))?        # e.g. 12h
                               (?:(?P<minutes>[0-9]{1,5})(?:minutes?|m))?    # e.g. 10m
                               (?:(?P<seconds>[0-9]{1,5})(?:seconds?|s))?    # e.g. 15s
                            """,
          re.VERBOSE,
      )
  
      def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None):
          match = self.compiled.fullmatch(argument)
          if match is None or not match.group(0):
              raise commands.BadArgument("invalid time provided")
  
          data = {k: int(v) for k, v in match.groupdict(default=0).items()}
          now = now or datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
          self.dt = now + relativedelta(**data)
  
      @classmethod
      async def convert(cls, ctx: Context, argument: str) -> ShortTime:
          return cls(argument, now=ctx.message.created_at)
  
  
  class HumanTime:
      dt: datetime.datetime
      calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE)
  
      def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None):
          now = now or datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
          dt, status = self.calendar.parseDT(argument, sourceTime=now)
          if not status.hasDateOrTime:
              raise commands.BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"')
  
          if not status.hasTime:
              # replace it with the current time
              dt = dt.replace(
                  hour=now.hour,
                  minute=now.minute,
                  second=now.second,
                  microsecond=now.microsecond,
                  tzinfo=datetime.timezone.utc,
              )
  
          self.dt = dt
          self._past = dt < now
  
      @classmethod
      async def convert(cls, ctx: Context, argument: str) -> HumanTime:
          return cls(argument, now=ctx.message.created_at)
  
  
  class Time(HumanTime):
      def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None):
          try:
              o = ShortTime(argument, now=now)
          except Exception:
              super().__init__(argument)
          else:
              self.dt = o.dt
              self._past = False
  
  
  class FutureTime(Time):
      def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None):
          super().__init__(argument, now=now)
  
          if self._past:
              raise commands.BadArgument("this time is in the past")
  
  
  class UserFriendlyTime(commands.Converter):
      """That way quotes aren't absolutely necessary."""
  
      def __init__(self, converter: commands.Converter = None, *, default: Optional[str] = None):
          if isinstance(converter, type) and issubclass(converter, commands.Converter):
              converter = converter()
  
          if converter is not None and not isinstance(converter, commands.Converter):
              raise TypeError("commands.Converter subclass necessary.")
  
          self.converter = converter
          self.default = default
  
      async def check_constraints(self, ctx: Context, now: datetime.datetime, remaining: str) -> UserFriendlyTime:
          if self.dt < now:
              raise commands.BadArgument("This time is in the past.")
  
          if not remaining:
              if self.default is None:
                  raise commands.BadArgument("Missing argument after the time.")
              remaining = self.default
  
          if self.converter is not None:
              self.arg = await self.converter.convert(ctx, remaining)
          else:
              self.arg = remaining
          return self
  
      def copy(self) -> UserFriendlyTime:
          cls = self.__class__
          obj = cls.__new__(cls)
          obj.converter = self.converter
          obj.default = self.default
          return obj
  
      async def convert(self, ctx: Context, argument: str) -> UserFriendlyTime:
          # Create a copy of ourselves to prevent race conditions from two
          # events modifying the same instance of a converter
          result = self.copy()
          try:
              calendar = HumanTime.calendar
              regex = ShortTime.compiled
              now = ctx.message.created_at
  
              match = regex.match(argument)
              if match is not None and match.group(0):
                  data = {k: int(v) for k, v in match.groupdict(default=0).items()}
                  remaining = argument[match.end() :].strip()
                  result.dt = now + relativedelta(**data)
                  return await result.check_constraints(ctx, now, remaining)
  
              # apparently nlp does not like "from now"
              # it likes "from x" in other cases though so let me handle the 'now' case
              if argument.endswith("from now"):
                  argument = argument[:-8].strip()
  
              if argument[0:2] == "me":
                  # starts with "me to", "me in", or "me at "
                  if argument[0:6] in ("me to ", "me in ", "me at "):
                      argument = argument[6:]
  
              elements = calendar.nlp(argument, sourceTime=now)
              if elements is None or len(elements) == 0:
                  raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')
  
              # handle the following cases:
              # "date time" foo
              # date time foo
              # foo date time
  
              # first the first two cases:
              dt, status, begin, end, _ = elements[0]
  
              if not status.hasDateOrTime:
                  raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')
  
              if begin not in (0, 1) and end != len(argument):
                  raise commands.BadArgument(
                      "Time is either in an inappropriate location, which "
                      "must be either at the end or beginning of your input, "
                      "or I just flat out did not understand what you meant. Sorry."
                  )
  
              if not status.hasTime:
                  # replace it with the current time
                  dt = dt.replace(
                      hour=now.hour,
                      minute=now.minute,
                      second=now.second,
                      microsecond=now.microsecond,
                  )
  
              # if midnight is provided, just default to next day
              if status.accuracy == pdt.pdtContext.ACU_HALFDAY:
                  dt = dt.replace(day=now.day + 1)
  
              result.dt = dt.replace(tzinfo=datetime.timezone.utc)
  
              if begin in (0, 1):
                  if begin == 1:
                      # check if it's quoted:
                      if argument[0] != '"':
                          raise commands.BadArgument("Expected quote before time input...")
  
                      if not (end < len(argument) and argument[end] == '"'):
                          raise commands.BadArgument("If the time is quoted, you must unquote it.")
  
                      remaining = argument[end + 1 :].lstrip(" ,.!")
                  else:
                      remaining = argument[end:].lstrip(" ,.!")
              elif len(argument) == end:
                  remaining = argument[:begin].strip()
  
              return await result.check_constraints(ctx, now, remaining)
          except Exception:
              import traceback
  
              traceback.print_exc()
              raise
  
  
  def human_timedelta(
      dt: datetime.datetime,
      *,
      source: datetime.datetime = None,
      accuracy: int = 3,
      brief: bool = False,
      suffix: bool = True,
  ) -> str:
      now = source or (datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc))
      # Microsecond free zone
      now = now.replace(microsecond=0)
      dt = dt.replace(microsecond=0)
  
      # This implementation uses relativedelta instead of the much more obvious
      # divmod approach with seconds because the seconds approach is not entirely
      # accurate once you go over 1 week in terms of accuracy since you have to
      # hardcode a month as 30 or 31 days.
      # A query like "11 months" can be interpreted as "!1 months and 6 days"
      if dt > now:
          delta = relativedelta(dt, now)
          suffix = ""
      else:
          delta = relativedelta(now, dt)
          suffix = " ago" if suffix else ""
  
      attrs: List[Tuple[str, str]] = [
          ("year", "y"),
          ("month", "mo"),
          ("day", "d"),
          ("hour", "h"),
          ("minute", "m"),
          ("second", "s"),
      ]
  
      output = []
      for attr, brief_attr in attrs:
          elem = getattr(delta, attr + "s")
          if not elem:
              continue
  
          if attr == "day":
              weeks = delta.weeks
              if weeks:
                  elem -= weeks * 7
                  if not brief:
                      output.append(format(plural(weeks), "week"))
                  else:
                      output.append(f"{weeks}w")
  
          if elem <= 0:
              continue
  
          if brief:
              output.append(f"{elem}{brief_attr}")
          else:
              output.append(format(plural(elem), attr))
  
      if accuracy is not None:
          output = output[:accuracy]
  
      if len(output) == 0:
          return "now"
      else:
          if not brief:
              return human_join(output, final="and") + suffix
          else:
              return " ".join(output) + suffix
  
  
  def hf_time(dt: datetime.datetime) -> str:
      date_modif = ordinal(dt.day)
      return dt.strftime(f"%A {date_modif} of %B %Y @ %H:%M %Z (%z)")
  
  
  def ordinal(number: int) -> str:
      return f"{number}{'tsnrhtdd'[(number//10%10!=1)*(number%10<4)*number%10::4]}"
  
  
  def format_relative(dt: datetime.datetime) -> str:
      return format_dt(dt, "R")
  