#  Applies a parameterized block to each element in a sequence of selected entries from the first
#  argument and returns the first argument.
#
# This function takes two mandatory arguments: the first should be an Array or a Hash or something that is
# of enumerable type (integer, Integer range, or String), and the second
# a parameterized block as produced by the puppet syntax:
#
#       $a.each |$x| { ... }
#       each($a) |$x| { ... }
#
# When the first argument is an Array (or of enumerable type other than Hash), the parameterized block
# should define one or two block parameters.
# For each application of the block, the next element from the array is selected, and it is passed to
# the block if the block has one parameter. If the block has two parameters, the first is the elements
# index, and the second the value. The index starts from 0.
#
#       $a.each |$index, $value| { ... }
#       each($a) |$index, $value| { ... }
#
# When the first argument is a Hash, the parameterized block should define one or two parameters.
# When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`,
# and when two parameters are defined the iteration is performed with key and value.
#
#       $a.each |$entry|       { ..."key ${$entry[0]}, value ${$entry[1]}" }
#       $a.each |$key, $value| { ..."key ${key}, value ${value}" }
#
# @example using each
#
#       [1,2,3].each |$val| { ... }                       # 1, 2, 3
#       [5,6,7].each |$index, $val| { ... }               # (0, 5), (1, 6), (2, 7)
#       {a=>1, b=>2, c=>3}].each |$val| { ... }           # ['a', 1], ['b', 2], ['c', 3]
#       {a=>1, b=>2, c=>3}.each |$key, $val| { ... }      # ('a', 1), ('b', 2), ('c', 3)
#       Integer[ 10, 20 ].each |$index, $value| { ... }   # (0, 10), (1, 11) ...
#       "hello".each |$char| { ... }                      # 'h', 'e', 'l', 'l', 'o'
#       3.each |$number| { ... }                          # 0, 1, 2
#
# @since 4.0.0
#
Puppet::Functions.create_function(:each) do
  dispatch :foreach_Hash_2 do
    param 'Hash[Any, Any]', :hash
    block_param 'Callable[2,2]', :block
  end

  dispatch :foreach_Hash_1 do
    param 'Hash[Any, Any]', :hash
    block_param 'Callable[1,1]', :block
  end

  dispatch :foreach_Enumerable_2 do
    param 'Any', :enumerable
    block_param 'Callable[2,2]', :block
  end

  dispatch :foreach_Enumerable_1 do
    param 'Any', :enumerable
    block_param 'Callable[1,1]', :block
  end

  def foreach_Hash_1(hash)
    enumerator = hash.each_pair
    hash.size.times do
      yield(enumerator.next)
    end
    # produces the receiver
    hash
  end

  def foreach_Hash_2(hash)
    enumerator = hash.each_pair
    hash.size.times do
      yield(*enumerator.next)
    end
    # produces the receiver
    hash
  end

  def foreach_Enumerable_1(enumerable)
    enum = asserted_enumerable(enumerable)
      begin
        loop { yield(enum.next) }
      rescue StopIteration
      end
    # produces the receiver
    enumerable
  end

  def foreach_Enumerable_2(enumerable)
    enum = asserted_enumerable(enumerable)
    index = 0
    begin
      loop do
        yield(index, enum.next)
        index += 1
      end
    rescue StopIteration
    end
    # produces the receiver
    enumerable
  end

  def asserted_enumerable(obj)
    unless enum = Puppet::Pops::Types::Enumeration.enumerator(obj)
      raise ArgumentError, ("#{self.class.name}(): wrong argument type (#{obj.class}; must be something enumerable.")
    end
    enum
  end

end
