defmodule Livebook.Utils.Time do
  # A simplified version of https://gist.github.com/tlemens/88e9b08f62150ba6082f478a4a03ac52

  @doc """
  Formats the given point in time relatively to present.

  ## Examples

  To deal with clock-drifts when receiving timestamps from the server, we accept future times:

        iex> Livebook.Utils.Time.time_ago_in_words(~N[2100-06-20 18:15:00])
        "less than 5 seconds"

  """
  @spec time_ago_in_words(NaiveDateTime.t()) :: String.t()
  def time_ago_in_words(naive_date_time) when is_struct(naive_date_time, NaiveDateTime) do
    distance_of_time_in_words(naive_date_time, NaiveDateTime.utc_now())
  end

  @doc """
  Formats time distance between `from_ndt` and `to_ndt` as a
  human-readable string.

  ## Examples

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:04])
      "less than 5 seconds"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:09])
      "less than 10 seconds"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:19])
      "less than 20 seconds"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:20])
      "half a minute"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:39])
      "half a minute"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:40])
      "less than a minute"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:59])
      "less than a minute"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:16:00])
      "1 minute"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:16:29])
      "1 minute"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:16:30])
      "2 minutes"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:58:30])
      "44 minutes"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:59:30])
      "about 1 hour"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 19:59:30])
      "about 2 hours"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-21 18:14:00])
      "about 24 hours"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-21 18:15:00])
      "1 day"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-22 18:15:00])
      "2 days"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-07-22 18:15:00])
      "about 1 month"

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2021-08-22 18:15:00])
      "about 14 months"

  To deal with clock-drifts when receiving timestamps from another machine, we accept future times:

      iex> Livebook.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:06], ~N[2020-06-20 18:15:04])
      "less than 5 seconds"

  """
  @spec distance_of_time_in_words(NaiveDateTime.t(), NaiveDateTime.t()) :: String.t()
  def distance_of_time_in_words(from_ndt, to_ndt)
      when is_struct(from_ndt, NaiveDateTime) and is_struct(to_ndt, NaiveDateTime) do
    duration_seconds = NaiveDateTime.diff(to_ndt, from_ndt)

    {:seconds, duration_seconds}
    |> maybe_convert_to_minutes()
    |> duration_in_words()
  end

  defp maybe_convert_to_minutes({:seconds, seconds}) when seconds > 59 do
    {:minutes, round(seconds / 60)}
  end

  defp maybe_convert_to_minutes(duration), do: duration

  defp duration_in_words({:seconds, seconds}) when seconds <= 4 do
    "less than 5 seconds"
  end

  defp duration_in_words({:seconds, seconds}) when seconds in 5..9 do
    "less than 10 seconds"
  end

  defp duration_in_words({:seconds, seconds}) when seconds in 10..19 do
    "less than 20 seconds"
  end

  defp duration_in_words({:seconds, seconds}) when seconds in 20..39 do
    "half a minute"
  end

  defp duration_in_words({:seconds, seconds}) when seconds in 40..59 do
    "less than a minute"
  end

  defp duration_in_words({:minutes, minutes}) when minutes == 1 do
    "1 minute"
  end

  defp duration_in_words({:minutes, minutes}) when minutes in 2..44 do
    "#{minutes} minutes"
  end

  defp duration_in_words({:minutes, minutes}) when minutes in 45..89 do
    "about 1 hour"
  end

  # 90 mins up to 24 hours
  defp duration_in_words({:minutes, minutes}) when minutes in 90..1439 do
    "about #{round(minutes / 60)} hours"
  end

  # 24 hours up to 42 hours
  defp duration_in_words({:minutes, minutes}) when minutes in 1440..2519 do
    "1 day"
  end

  # 42 hours up to 30 days
  defp duration_in_words({:minutes, minutes}) when minutes in 2520..43_199 do
    "#{round(minutes / 1440)} days"
  end

  # 30 days up to 45 days
  defp duration_in_words({:minutes, minutes}) when minutes in 43_200..64_799 do
    "about 1 month"
  end

  # 45 days up to 60 days
  defp duration_in_words({:minutes, minutes}) when minutes in 64_800..86_399 do
    "about 2 months"
  end

  defp duration_in_words({:minutes, minutes}) do
    "about #{round(minutes / 43_200)} months"
  end
end
