# `Time::Span` represents one period of time.
#
# A `Time::Span` initializes with the specified period.
# Different numbers of arguments generate a `Time::Span` in different length.
# Check all `#new` methods for details.
#
# ```
# Time::Span.new(nanoseconds: 10_000)                           # => 00:00:00.000010000
# Time::Span.new(hours: 10, minutes: 10, seconds: 10)           # => 10:10:10
# Time::Span.new(days: 10, hours: 10, minutes: 10, seconds: 10) # => 10.10:10:10
# ```
#
# Calculation between `Time` also returns a `Time::Span`.
#
# ```
# span = Time.utc(2015, 10, 10) - Time.utc(2015, 9, 10)
# span       # => 30.00:00:00
# span.class # => Time::Span
# ```
#
# Inspection:
#
# ```
# span = Time::Span.new(hours: 20, minutes: 10, seconds: 10)
# span.hours   # => 20
# span.minutes # => 10
# span.seconds # => 10
# ```
#
# Calculation:
#
# ```
# a = Time::Span.new(hours: 20, minutes: 10, seconds: 10)
# b = Time::Span.new(hours: 10, minutes: 10, seconds: 10)
# c = a - b # => 10:00:00
# c.hours   # => 10
# ```
#
struct Time::Span
  # *Heavily* inspired by Mono's Time::Span class:
  # https://github.com/mono/mono/blob/master/mcs/class/corlib/System/Time::Span.cs

  include Comparable(self)
  include Steppable

  MAX  = new seconds: Int64::MAX, nanoseconds: 999_999_999
  MIN  = new seconds: Int64::MIN, nanoseconds: -999_999_999
  ZERO = new nanoseconds: 0

  @seconds : Int64

  # Nanoseconds are always in the range (-999_999_999..999_999_999)
  # and always have the same sign as @seconds (if seconds is zero,
  # @nanoseconds can either be negative or positive).
  @nanoseconds : Int32

  # Creates a new `Time::Span` from *seconds* and *nanoseconds*.
  #
  # Nanoseconds get normalized in the range of `0...1_000_000_000`,
  # the nanosecond overflow gets added as seconds.
  #
  # ```
  # Time::Span.new(seconds: 30)                 # => 00:00:30
  # Time::Span.new(seconds: 5, nanoseconds: 12) # => 00:00:05.000000012
  # ```
  def initialize(*, seconds : Int, nanoseconds : Int)
    # Normalize nanoseconds in the range 0...1_000_000_000
    @seconds = seconds.to_i64 + nanoseconds.tdiv(NANOSECONDS_PER_SECOND).to_i64
    @nanoseconds = nanoseconds.remainder(NANOSECONDS_PER_SECOND).to_i32

    # Make sure that if seconds is positive, nanoseconds is
    # positive too. Likewise, if seconds is negative, make
    # sure that nanoseconds is negative too.
    if @seconds > 0 && @nanoseconds < 0
      @seconds -= 1
      @nanoseconds += NANOSECONDS_PER_SECOND
    elsif @seconds < 0 && @nanoseconds > 0
      @seconds += 1
      @nanoseconds -= NANOSECONDS_PER_SECOND
    end
  end

  # Creates a new `Time::Span` from the *nanoseconds* given
  #
  # Nanoseconds get normalized in the range of `0...1_000_000_000`,
  # the nanosecond overflow gets added as seconds.
  #
  # ```
  # Time::Span.new(nanoseconds: 500_000_000)   # => 00:00:00.500000000
  # Time::Span.new(nanoseconds: 5_500_000_000) # => 00:00:05.500000000
  # ```
  def self.new(*, nanoseconds : Int)
    new(seconds: 0, nanoseconds: nanoseconds)
  end

  # Creates a new `Time::Span` from the *weeks, *days*, *hours*, *minutes*, *seconds* and *nanoseconds* given
  #
  # Any time unit can be omitted.
  #
  # ```
  # Time::Span.new(days: 1)                                                   # => 1.00:00:00
  # Time::Span.new(days: 1, hours: 2, minutes: 3)                             # => 1.02:03:00
  # Time::Span.new(days: 1, hours: 2, minutes: 3, seconds: 4, nanoseconds: 5) # => 1.02:03:04.000000005
  # ```
  def self.new(*, weeks : Int = 0, days : Int = 0, hours : Int = 0, minutes : Int = 0, seconds : Int = 0, nanoseconds : Int = 0)
    new(
      seconds: compute_seconds(weeks, days, hours, minutes, seconds),
      nanoseconds: nanoseconds,
    )
  end

  private def self.compute_seconds(weeks, days, hours, minutes, seconds)
    weeks_to_seconds = SECONDS_PER_WEEK.to_i64 * weeks
    days_to_seconds = SECONDS_PER_DAY.to_i64 * days
    hours_to_seconds = SECONDS_PER_HOUR.to_i64 * hours
    minutes_to_seconds = SECONDS_PER_MINUTE.to_i64 * minutes

    weeks_to_seconds + days_to_seconds + hours_to_seconds + minutes_to_seconds + seconds
  rescue OverflowError
    raise ArgumentError.new "Time::Span too big or too small"
  end

  # Returns the number of full days in this time span.
  #
  # ```
  # (5.days + 25.hours).days # => 6_i64
  # ```
  def days : Int64
    to_i.tdiv(SECONDS_PER_DAY)
  end

  # Returns the number of full hours of the day (`0..23`) in this time span.
  def hours : Int32
    to_i.remainder(SECONDS_PER_DAY)
      .tdiv(SECONDS_PER_HOUR)
      .to_i
  end

  # Returns the number of full minutes of the hour (`0..59`) in this time span.
  def minutes : Int32
    to_i.remainder(SECONDS_PER_HOUR)
      .tdiv(SECONDS_PER_MINUTE)
      .to_i
  end

  # Returns the number of full seconds of the minute (`0..59`) in this time span.
  def seconds : Int32
    to_i.remainder(SECONDS_PER_MINUTE)
      .to_i
  end

  # Returns the number of milliseconds of the second (`0..999`) in this time span.
  def milliseconds : Int32
    nanoseconds // NANOSECONDS_PER_MILLISECOND
  end

  # Returns the number of microseconds of the second (`0..999999`) in this time span.
  def microseconds : Int32
    nanoseconds // NANOSECONDS_PER_MICROSECOND
  end

  # Returns the number of nanoseconds of the second (`0..999_999_999`)
  # in this time span.
  def nanoseconds : Int32
    @nanoseconds
  end

  # Converts to a (possibly fractional) number of weeks.
  #
  # ```
  # (4.weeks + 5.days + 6.hours).total_weeks # => 4.75
  # ```
  def total_weeks : Float64
    total_days / 7
  end

  # Converts to a (possibly fractional) number of days.
  #
  # ```
  # (36.hours).total_days # => 1.5
  # ```
  def total_days : Float64
    total_hours / 24
  end

  # Converts to a (possibly fractional) number of hours.
  def total_hours : Float64
    total_minutes / 60
  end

  # Converts to a (possibly fractional) number of minutes.
  def total_minutes : Float64
    total_seconds / 60
  end

  # Converts to a (possibly fractional) number of seconds.
  def total_seconds : Float64
    to_i.to_f + (nanoseconds.to_f / NANOSECONDS_PER_SECOND)
  end

  # Converts to a number of milliseconds.
  def total_milliseconds : Float64
    total_nanoseconds / NANOSECONDS_PER_MILLISECOND
  end

  # Converts to a number of microseconds.
  def total_microseconds : Float64
    total_nanoseconds / NANOSECONDS_PER_MICROSECOND
  end

  # Converts to a number of nanoseconds.
  def total_nanoseconds : Float64
    (to_i.to_f * NANOSECONDS_PER_SECOND) + nanoseconds
  end

  # Alias of `total_seconds`.
  def to_f : Float64
    total_seconds
  end

  # Returns the number of full seconds.
  def to_i : Int64
    @seconds
  end

  # Returns the absolute (non-negative) amount of time this `Time::Span`
  # represents by removing the sign.
  def abs : Time::Span
    Span.new(seconds: to_i.abs, nanoseconds: nanoseconds.abs)
  end

  # Returns the sign of this time span.
  #
  # Values are `-1`, `0`, `1` if `self` is smaller, equal, bigger compared to `ZERO`.
  def sign : Int32
    if @seconds == 0
      @nanoseconds.sign
    else
      @seconds.sign
    end
  end

  # Returns a `Time` that happens later by `self` than the current time.
  def from_now : Time
    Time.local + self
  end

  # Returns a `Time` that happens earlier by `self` than the current time.
  def ago : Time
    Time.local - self
  end

  def -(other : self) : Time::Span
    # TODO check overflow
    Span.new(
      seconds: to_i - other.to_i,
      nanoseconds: nanoseconds - other.nanoseconds,
    )
  end

  def - : Time::Span
    # TODO check overflow
    Span.new(
      seconds: -to_i,
      nanoseconds: -nanoseconds,
    )
  end

  def +(other : self) : Time::Span
    # TODO check overflow
    Span.new(
      seconds: to_i + other.to_i,
      nanoseconds: nanoseconds + other.nanoseconds,
    )
  end

  # Returns the additive identity of this type.
  #
  # This is `zero`.
  def self.additive_identity : self
    zero
  end

  def + : self
    self
  end

  # Returns a `Time::Span` that is *number* times longer.
  def *(number : Int) : Time::Span
    # TODO check overflow
    Span.new(
      seconds: to_i * number,
      nanoseconds: nanoseconds.to_i64 * number,
    )
  end

  # Returns a `Time::Span` that is *number* times longer.
  def *(number : Float) : Time::Span
    (total_nanoseconds * number).nanoseconds
  end

  # Returns a `Time::Span` that is divided by *number*.
  def /(number : Int) : Time::Span
    seconds = to_i.tdiv(number)
    nanoseconds = self.nanoseconds.tdiv(number)

    remainder = to_i.remainder(number)
    nanoseconds += (remainder * NANOSECONDS_PER_SECOND) // number

    # TODO check overflow
    Span.new(
      seconds: seconds,
      nanoseconds: nanoseconds,
    )
  end

  # Returns a `Time::Span` that is divided by *number*.
  def /(number : Float) : Time::Span
    (total_nanoseconds / number).nanoseconds
  end

  def /(other : self) : Float64
    total_nanoseconds.to_f64 / other.total_nanoseconds.to_f64
  end

  def <=>(other : self)
    cmp = to_i <=> other.to_i
    cmp = nanoseconds <=> other.nanoseconds if cmp == 0
    cmp
  end

  def inspect(io : IO) : Nil
    if to_i < 0 || nanoseconds < 0
      io << '-'
    end

    # We need to take absolute values of all components.
    # Can't handle negative timespans by negating the Time::Span
    # as a whole. This would lead to an overflow for the
    # degenerate case `Time::Span.MinValue`.
    if days != 0
      io << days.abs
      io << '.'
    end

    hours = self.hours.abs
    io << '0' if hours < 10
    io << hours

    io << ':'

    minutes = self.minutes.abs
    io << '0' if minutes < 10
    io << minutes

    io << ':'

    seconds = self.seconds.abs
    io << '0' if seconds < 10
    io << seconds

    nanoseconds = self.nanoseconds.abs
    if nanoseconds != 0
      io << '.'
      io << '0' if nanoseconds < 100000000
      io << '0' if nanoseconds < 10000000
      io << '0' if nanoseconds < 1000000
      io << '0' if nanoseconds < 100000
      io << '0' if nanoseconds < 10000
      io << '0' if nanoseconds < 1000
      io << '0' if nanoseconds < 100
      io << '0' if nanoseconds < 10
      io << nanoseconds
    end
  end

  # Creates a new `Time::Span` representing a span of zero time.
  def self.zero : Time::Span
    ZERO
  end

  # Returns `true` if `self` represents a span of zero time.
  #
  # ```
  # 2.hours.zero?  # => false
  # 0.days.zero?   # => true
  # 1.second.zero? # => false
  # ```
  def zero? : Bool
    self == ZERO
  end

  # Returns `true` if `self` represents a positive time span.
  #
  # ```
  # 2.hours.positive? # => true
  # 0.days.positive?  # => false
  # -3.days.positive? # => false
  # ```
  def positive? : Bool
    self > ZERO
  end

  # Returns `true` if `self` represents a negative time span.
  #
  # ```
  # 2.hours.negative? # => false
  # 0.days.negative?  # => false
  # -3.days.negative? # => true
  # ```
  def negative? : Bool
    self < ZERO
  end
end

struct Int
  # Returns a `Time::Span` of `self` weeks.
  def weeks : Time::Span
    Time::Span.new(days: (7 * self))
  end

  # :ditto:
  def week : Time::Span
    weeks
  end

  # Returns a `Time::Span` of `self` days.
  def days : Time::Span
    Time::Span.new(days: self)
  end

  # :ditto:
  def day : Time::Span
    days
  end

  # Returns a `Time::Span` of `self` hours.
  def hours : Time::Span
    Time::Span.new(hours: self)
  end

  # :ditto:
  def hour : Time::Span
    hours
  end

  # Returns a `Time::Span` of `self` minutes.
  def minutes : Time::Span
    Time::Span.new(minutes: self)
  end

  # :ditto:
  def minute : Time::Span
    minutes
  end

  # Returns a `Time::Span` of `self` seconds.
  def seconds : Time::Span
    Time::Span.new(seconds: self)
  end

  # :ditto:
  def second : Time::Span
    seconds
  end

  # Returns a `Time::Span` of `self` milliseconds.
  def milliseconds : Time::Span
    sec, m = self.to_i64.divmod(1_000)
    Time::Span.new(seconds: sec, nanoseconds: m * 1_000_000)
  end

  # :ditto:
  def millisecond : Time::Span
    milliseconds
  end

  # Returns a `Time::Span` of `self` microseconds.
  def microseconds : Time::Span
    sec, m = self.to_i64.divmod(1_000_000)
    Time::Span.new(seconds: sec, nanoseconds: m * 1_000)
  end

  # :ditto:
  def microsecond : Time::Span
    microseconds
  end

  # Returns a `Time::Span` of `self` nanoseconds.
  def nanoseconds : Time::Span
    Time::Span.new(nanoseconds: self.to_i64)
  end

  # :ditto:
  def nanosecond : Time::Span
    nanoseconds
  end
end

struct Float
  # Returns a `Time::Span` of `self` weeks.
  def weeks : Time::Span
    (self * 7).days
  end

  # Returns a `Time::Span` of `self` days.
  def days : Time::Span
    (self * 24).hours
  end

  # Returns a `Time::Span` of `self` hours.
  def hours : Time::Span
    (self * 60).minutes
  end

  # Returns a `Time::Span` of `self` minutes.
  def minutes : Time::Span
    (self * 60).seconds
  end

  # Returns a `Time::Span` of `self` seconds.
  def seconds : Time::Span
    seconds = self.to_i64
    nanoseconds = (self - seconds) * Time::NANOSECONDS_PER_SECOND

    # round away from zero
    nanoseconds = (nanoseconds < 0 ? (nanoseconds - 0.5) : (nanoseconds + 0.5)).to_i64

    Time::Span.new(
      seconds: seconds,
      nanoseconds: nanoseconds,
    )
  end

  # Returns a `Time::Span` of `self` milliseconds.
  def milliseconds : Time::Span
    (self / 1_000).seconds
  end

  # Returns a `Time::Span` of `self` microseconds.
  def microseconds : Time::Span
    (self / 1_000_000).seconds
  end

  # Returns a `Time::Span` of `self` nanoseconds.
  def nanoseconds : Time::Span
    seconds = (self / Time::NANOSECONDS_PER_SECOND).to_i64
    nanoseconds = self.remainder(Time::NANOSECONDS_PER_SECOND)

    # round away from zero
    nanoseconds = (nanoseconds < 0 ? (nanoseconds - 0.5) : (nanoseconds + 0.5)).to_i64

    Time::Span.new(
      seconds: seconds,
      nanoseconds: nanoseconds,
    )
  end
end

# Represents a number of months passed. Used for shifting `Time`s by a
# specified number of months.
#
# ```
# Time.local(2016, 2, 1) + 13.months # => 2017-03-01 00:00:00
# Time.local(2016, 2, 29) + 2.years  # => 2018-02-28 00:00:00
# ```
struct Time::MonthSpan
  # The number of months.
  getter value : Int64

  def initialize(value : Int)
    @value = value.to_i64
  end

  # Returns a `Time` that happens N months after now.
  def from_now : Time
    Time.local + self
  end

  # Returns a `Time` that happens N months before now.
  def ago : Time
    Time.local - self
  end
end

struct Int
  # Returns a `Time::MonthSpan` of `self` months.
  def months : Time::MonthSpan
    Time::MonthSpan.new(self)
  end

  # :ditto:
  def month : Time::MonthSpan
    months
  end

  # Returns a `Time::MonthSpan` of `self` years.
  def years : Time::MonthSpan
    Time::MonthSpan.new(self * 12)
  end

  # :ditto:
  def year : Time::MonthSpan
    years
  end
end
