# A `Range` represents an interval: a set of values with a beginning and an end.
#
# Ranges may be constructed using the usual `new` method or with literals:
#
# ```
# x..y  # an inclusive range, in mathematics: [x, y]
# x...y # an exclusive range, in mathematics: [x, y)
# (x..) # an endless range, in mathematics: >= x
# ..y   # a beginless inclusive range, in mathematics: <= y
# ...y  # a beginless exclusive range, in mathematics: < y
# ```
#
# See [`Range` literals](https://crystal-lang.org/reference/syntax_and_semantics/literals/range.html) in the language reference.
#
# An easy way to remember which one is inclusive and which one is exclusive it
# to think of the extra dot as if it pushes *y* further away, thus leaving it outside of the range.
#
# Ranges typically involve integers, but can be created using arbitrary objects
# as long as they define `succ` (or `pred` for `reverse_each`), to get the
# next element in the range, and `<` and `==`, to know when the range reached the end:
#
# ```
# # Represents a string of 'x's.
# struct Xs
#   include Comparable(Xs)
#
#   getter size
#
#   def initialize(@size : Int32)
#   end
#
#   def succ
#     Xs.new(@size + 1)
#   end
#
#   def <=>(other)
#     @size <=> other.size
#   end
#
#   def inspect(io)
#     @size.times { io << 'x' }
#   end
#
#   def to_s(io)
#     io << @size << ' '
#     inspect(io)
#   end
# end
# ```
#
# An example of using `Xs` to construct a range:
#
# ```
# r = Xs.new(3)..Xs.new(6)
# r.to_s                 # => "xxx..xxxxxx"
# r.to_a                 # => [Xs.new(3), Xs.new(4), Xs.new(5), Xs.new(6)]
# r.includes?(Xs.new(5)) # => true
# ```
struct Range(B, E)
  include Enumerable(B)
  include Iterable(B)

  # Returns the object that defines the beginning of this range.
  #
  # ```
  # (1..10).begin  # => 1
  # (1...10).begin # => 1
  # ```
  getter begin : B

  # Returns the object that defines the end of the range.
  #
  # ```
  # (1..10).end  # => 10
  # (1...10).end # => 10
  # ```
  getter end : E

  # Returns `true` if the range is exclusive.
  # Returns `false` otherwise (default).
  getter? exclusive : Bool

  # Constructs a `Range` using the given beginning and end.
  #
  # ```
  # Range.new(1, 10)                  # => 1..10
  # Range.new(1, 10, exclusive: true) # => 1...10
  # ```
  def initialize(@begin : B, @end : E, @exclusive : Bool = false)
  end

  def ==(other : Range)
    @begin == other.@begin && @end == other.@end && @exclusive == other.@exclusive
  end

  # Returns an `Iterator` that cycles over the values of this range.
  #
  # ```
  # (1..3).cycle.first(5).to_a # => [1, 2, 3, 1, 2]
  # ```
  def cycle
    each.cycle
  end

  # Iterates over the elements of this range, passing each in turn to the block.
  #
  # ```
  # (10..15).each { |n| print n, ' ' }
  # # prints: 10 11 12 13 14 15
  # ```
  def each(&) : Nil
    current = @begin
    if current.nil?
      raise ArgumentError.new("Can't each beginless range")
    end

    # TODO: This typeof and the macro interpolations are a workaround until #9324 is fixed.
    typeof(yield current)

    {% if E == Nil %}
      while true
        {{ "yield current".id }}
        current = current.succ
      end
    {% else %}
      end_value = @end
      while end_value.nil? || current < end_value
        {{ "yield current".id }}
        current = current.succ
      end
      {{ "yield current".id }} if !@exclusive && current == end_value
    {% end %}
  end

  # Returns an `Iterator` over the elements of this range.
  #
  # ```
  # (1..3).each.skip(1).to_a # => [2, 3]
  # ```
  def each
    if @begin.nil?
      raise ArgumentError.new("Can't each beginless range")
    end

    ItemIterator.new(self)
  end

  # Iterates over the elements of this range in reverse order,
  # passing each in turn to the block.
  #
  # ```
  # (10...15).reverse_each { |n| print n, ' ' }
  # # prints: 14 13 12 11 10
  # ```
  def reverse_each(&) : Nil
    end_value = @end
    if end_value.nil?
      raise ArgumentError.new("Can't reverse_each endless range")
    end

    begin_value = @begin

    yield end_value if !@exclusive && (begin_value.nil? || !(end_value < begin_value))
    current = end_value

    {% if B == Nil %}
      while true
        current = current.pred
        yield current
      end
    {% else %}
      while begin_value.nil? || begin_value < current
        current = current.pred
        yield current
      end
    {% end %}
  end

  # Returns a reverse `Iterator` over the elements of this range.
  #
  # ```
  # (1..3).reverse_each.skip(1).to_a # => [2, 1]
  # ```
  def reverse_each
    if @end.nil?
      raise ArgumentError.new("Can't reverse_each endless range")
    end

    ReverseIterator.new(self)
  end

  # Iterates from `begin` to `end` incrementing by the amount of *step* on each
  # iteration.
  #
  # ```
  # ary = [] of Int32
  # (1..4).step(by: 2) do |x|
  #   ary << x
  # end
  # ary                      # => [1, 3]
  # (1..4).step(by: 2).to_a  # => [1, 3]
  # (1..4).step(by: 1).to_a  # => [1, 2, 3, 4]
  # (1...4).step(by: 1).to_a # => [1, 2, 3]
  # ```
  #
  # If `B` is a `Steppable`, implementation is delegated to `Steppable#step`.
  # Otherwise `#succ` method is expected to be defined on `begin` and its
  # successors and iteration is based on calling `#succ` sequentially
  # (*step* times per iteration).
  #
  # Raises `ArgumentError` if `begin` is `nil`.
  def step(by = 1, &) : Nil
    current = @begin
    if current.nil?
      raise ArgumentError.new("Can't step beginless range")
    end

    {% if B < Steppable %}
      current.step(to: @end, by: by, exclusive: @exclusive) do |x|
        yield x
      end
    {% else %}
      end_value = @end
      while end_value.nil? || current < end_value
        yield current
        by.times do
          current = current.succ
          return if end_value && current > end_value
        rescue exc : OverflowError
          if current == end_value
            return
          else
            raise exc
          end
        end
      end
      yield current if !@exclusive && current == @end
    {% end %}
  end

  # :ditto:
  def step(by = 1) : Iterator
    start = @begin
    if start.nil?
      raise ArgumentError.new("Can't step beginless range")
    end

    {% if B < Steppable %}
      start.step(to: @end, by: by, exclusive: @exclusive)
    {% else %}
      StepIterator(self, B, typeof(by)).new(self, by)
    {% end %}
  end

  # Returns `true` if this range excludes the *end* element.
  #
  # ```
  # (1..10).excludes_end?  # => false
  # (1...10).excludes_end? # => true
  # ```
  def excludes_end? : Bool
    @exclusive
  end

  # Returns `true` if this range includes the given *value*.
  #
  # ```
  # (1..10).includes?(4)  # => true
  # (1..10).includes?(10) # => true
  # (1..10).includes?(11) # => false
  #
  # (1...10).includes?(9)  # => true
  # (1...10).includes?(10) # => false
  # ```
  def includes?(value) : Bool
    begin_value = @begin
    end_value = @end

    # begin passes
    (begin_value.nil? || value >= begin_value) &&
      # end passes
      (end_value.nil? ||
        (@exclusive ? value < end_value : value <= end_value))
  end

  # Same as `includes?`.
  def covers?(value)
    includes?(value)
  end

  # Same as `includes?`, useful for the `case` expression.
  #
  # ```
  # case 79
  # when 1..50   then puts "low"
  # when 51..75  then puts "medium"
  # when 76..100 then puts "high"
  # end
  # ```
  #
  # Produces:
  #
  # ```text
  # high
  # ```
  #
  # See also: `Object#===`.
  def ===(value)
    includes?(value)
  end

  def to_s(io : IO) : Nil
    @begin.try &.inspect(io)
    io << (@exclusive ? "..." : "..")
    @end.try &.inspect(io)
  end

  def inspect(io : IO) : Nil
    to_s(io)
  end

  # Optimized version of `Enumerable#sum` that runs in O(1) time when `self` is
  # an `Int` range.
  def sum(initial)
    b = self.begin
    e = self.end

    if b.is_a?(Int) && e.is_a?(Int)
      e -= 1 if @exclusive
      n = e - b + 1
      if n >= 0
        initial + n * (b + e) // 2
      else
        initial
      end
    else
      super
    end
  end

  # Optimized version of `Enumerable#sample` that runs in O(1) time when `self`
  # is an `Int` or `Float` range. In these cases, this range is considered to be
  # a distribution of numeric values rather than a collection of elements, and
  # the method simply calls `random.rand(self)`.
  #
  # Raises `ArgumentError` if `self` is an open range.
  def sample(random : Random? = nil)
    rng = random || Random.thread_default

    {% if B < Int && E < Int %}
      rng.rand(self)
    {% elsif B < Float && E < Float %}
      rng.rand(self)
    {% elsif B.nilable? || E.nilable? %}
      b = self.begin
      e = self.end

      if b.nil? || e.nil?
        raise ArgumentError.new("Can't sample an open range")
      end

      Range.new(b, e, @exclusive).sample(rng)
    {% else %}
      super(rng)
    {% end %}
  end

  # :inherit:
  #
  # If `self` is not empty and `n` is equal to 1, calls `sample(random)` exactly
  # once. Thus, *random* will be left in a different state compared to the
  # implementation in `Enumerable`.
  def sample(n : Int, random : Random? = nil)
    rng = random || Random.thread_default

    if self.begin.nil? || self.end.nil?
      raise ArgumentError.new("Can't sample an open range")
    end

    if n < 0
      raise ArgumentError.new "Can't sample negative number of elements"
    end

    # For a range of integers we can do much better
    {% if B < Int && E < Int %}
      min = self.begin
      max = self.end

      if exclusive? ? max <= min : max < min
        raise ArgumentError.new "Invalid range for rand: #{self}"
      end

      max -= 1 if exclusive?

      available = max - min + 1

      # When a big chunk of elements is going to be needed, it's
      # faster to just traverse the entire range than hitting
      # a lot of duplicates because or random.
      if n >= available // 4
        return super(n, rng)
      end

      possible = Math.min(n, available)

      # If we must return all values in the range...
      if possible == available
        result = Array(B).new(possible) { |i| min + i }
        result.shuffle!(rng)
        return result
      end

      range_sample(n, rng)
    {% elsif B < Float && E < Float %}
      min = self.begin
      max = self.end

      if exclusive? ? max <= min : max < min
        raise ArgumentError.new "Invalid range for rand: #{self}"
      end

      if min == max
        return [min]
      end

      range_sample(n, rng)
    {% else %}
      case n
      when 0
        [] of B
      when 1
        [sample(rng)]
      else
        super(n, rng)
      end
    {% end %}
  end

  private def range_sample(n, random)
    if n <= 16
      # For a small requested amount doing a linear lookup is faster
      result = Array(B).new(n)
      until result.size == n
        value = sample(random)
        result << value unless result.includes?(value)
      end
      result
    else
      # Otherwise using a Set is faster
      result = Set(B).new(n)
      until result.size == n
        result << sample(random)
      end
      result.to_a
    end
  end

  # Returns a new `Range` with `begin` and `end` cloned.
  def clone
    Range.new(@begin.clone, @end.clone, @exclusive)
  end

  def map(&block : B -> U) forall U
    b = self.begin
    e = self.end

    # Optimized implementation for int range
    if b.is_a?(Int) && e.is_a?(Int)
      e -= 1 if @exclusive
      n = e - b + 1
      n = 0 if n < 0
      Array(U).new(n) { |i| yield b + i }
    else
      super { |i| yield i }
    end
  end

  # Returns the number of values in this range.
  #
  # If both the beginning and the end of this range are `Int`s, runs in constant
  # time instead of linear.
  #
  # ```
  # (3..8).size  # => 6
  # (3...8).size # => 5
  # ```
  #
  # Raises `OverflowError` if the difference is bigger than `Int32`.
  # Raises `ArgumentError` if either `begin` or `end` are `nil`.
  def size : Int32
    b = self.begin
    e = self.end

    # Optimized implementation for int range
    if b.is_a?(Int) && e.is_a?(Int)
      return 0 if e < b

      # Convert `e` to `Int32` in order to ensure that `e &- b` doesn't get
      # truncated due to the smaller type of `e`.
      if e.is_a?(UInt8 | Int8 | UInt16 | Int16)
        e = e.to_i32!
      end

      diff = (e &- b).to_i32.abs
      diff &+= 1 unless @exclusive
      diff
    else
      if b.nil? || e.nil?
        raise ArgumentError.new("Can't calculate size of an open range")
      end
      super
    end
  end

  private class ItemIterator(B, E)
    include Iterator(B)

    @range : Range(B, E)
    @current : B
    @reached_end : Bool

    def initialize(@range : Range(B, E), @current = range.begin, @reached_end = false)
    end

    def next
      return stop if @reached_end

      end_value = @range.end

      if end_value.nil? || @current < end_value
        value = @current
        @current = @current.succ
        value
      else
        @reached_end = true

        if !@range.excludes_end? && @current == end_value
          @current
        else
          stop
        end
      end
    end
  end

  private class ReverseIterator(B, E)
    include Iterator(E)

    @range : Range(B, E)
    @current : E

    def initialize(@range : Range(B, E))
      if range.excludes_end?
        @current = range.end.not_nil!
      else
        @current = range.end.not_nil!.succ
      end
    end

    def next
      begin_value = @range.begin

      return stop if !begin_value.nil? && @current <= begin_value
      @current = @current.pred
    end
  end

  private class StepIterator(R, B, N)
    include Iterator(B)

    @range : R
    @step : N
    @current : B
    @reached_end : Bool
    @at_start = true

    def initialize(@range, @step, @current = range.begin, @reached_end = false)
    end

    def next
      return stop if @reached_end

      end_value = @range.end

      if @at_start
        @at_start = false

        if end_value
          if @current > end_value || (@current == end_value && @range.exclusive?)
            @reached_end = true
            return stop
          end
        end

        return @current
      end

      if end_value.nil? || @current < end_value
        @step.times do
          if end_value && @current >= end_value
            @reached_end = true
            return stop
          end

          @current = @current.succ
        end

        if @current == end_value && @range.exclusive?
          @reached_end = true
          stop
        else
          @current
        end
      else
        @reached_end = true
        stop
      end
    end

    def sum(initial)
      return super if @reached_end

      b = @current
      e = @range.end
      d = @step

      if b.is_a?(Int) && e.is_a?(Int) && d.is_a?(Int)
        e -= 1 if @range.excludes_end?
        n = (e - b) // d + 1
        if n >= 0
          e = b + (n - 1) * d
          initial + n * (b + e) // 2
        else
          initial
        end
      else
        super
      end
    end
  end
end

require "./range/*"
