# frozen_string_literal: true

# Produces a unique set of values from an `Iterable` argument.
#
# * If the argument is a `String`, the unique set of characters are returned as a new `String`.
# * If the argument is a `Hash`, the resulting hash associates a set of keys with a set of unique values.
# * For all other types of `Iterable` (`Array`, `Iterator`) the result is an `Array` with
#   a unique set of entries.
# * Comparison of all `String` values are case sensitive.
# * An optional code block can be given - if present it is given each candidate value and its return is used instead of the given value. This
#   enables transformation of the value before comparison. The result of the lambda is only used for comparison.
# * The optional code block when used with a hash is given each value (not the keys).
#
# @example Using unique with a String
#
# ```puppet
# # will produce 'abc'
# "abcaabb".unique
# ```
#
# @example Using unique with an Array
#
# ```puppet
# # will produce ['a', 'b', 'c']
# ['a', 'b', 'c', 'a', 'a', 'b'].unique
# ```
#
# @example Using unique with a Hash
#
# ```puppet
# # will produce { ['a', 'b'] => [10], ['c'] => [20]}
# {'a' => 10, 'b' => 10, 'c' => 20}.unique
#
# # will produce { 'a' => 10, 'c' => 20 } (use first key with first value)
# Hash.new({'a' => 10, 'b' => 10, 'c' => 20}.unique.map |$k, $v| { [ $k[0] , $v[0]] })
#
# # will produce { 'b' => 10, 'c' => 20 } (use last key with first value)
# Hash.new({'a' => 10, 'b' => 10, 'c' => 20}.unique.map |$k, $v| { [ $k[-1] , $v[0]] })
# ```
#
# @example Using unique with an Iterable
#
# ```
# # will produce [3, 2, 1]
# [1,2,2,3,3].reverse_each.unique
# ```
#
# @example Using unique with a lambda
#
# ```puppet
# # will produce [['sam', 'smith'], ['sue', 'smith']]
# [['sam', 'smith'], ['sam', 'brown'], ['sue', 'smith']].unique |$x| { $x[0] }
#
# # will produce [['sam', 'smith'], ['sam', 'brown']]
# [['sam', 'smith'], ['sam', 'brown'], ['sue', 'smith']].unique |$x| { $x[1] }
#
# # will produce ['aBc', 'bbb'] (using a lambda to make comparison using downcased (%d) strings)
# ['aBc', 'AbC', 'bbb'].unique |$x| { String($x,'%d') }
#
# # will produce {[a] => [10], [b, c, d, e] => [11, 12, 100]}
# {a => 10, b => 11, c => 12, d => 100, e => 11}.unique |$v| { if $v > 10 { big } else { $v } }
# ```
#
# Note that for `Hash` the result is slightly different than for the other data types. For those the result contains the
# *first-found* unique value, but for `Hash` it contains associations from a set of keys to the set of values clustered by the
# equality lambda (or the default value equality if no lambda was given). This makes the `unique` function more versatile for hashes
# in general, while requiring that the simple computation of "hash's unique set of values" is performed as `$hsh.map |$k, $v| { $v }.unique`.
# (Generally, it's meaningless to compute the unique set of hash keys because they are unique by definition. However, the
# situation can change if the hash keys are processed with a different lambda for equality. For this unique computation,
# first map the hash to an array of its keys.)
# If the more advanced clustering is wanted for one of the other data types, simply transform it into a `Hash` as shown in the
# following example.
#
# @example turning a string or array into a hash with index keys
#
# ```puppet
# # Array ['a', 'b', 'c'] to Hash with index results in
# # {0 => 'a', 1 => 'b', 2 => 'c'}
# Hash(['a', 'b', 'c'].map |$i, $v| { [$i, $v]})
#
# # String "abc" to Hash with index results in
# # {0 => 'a', 1 => 'b', 2 => 'c'}
# Hash(Array("abc").map |$i,$v| { [$i, $v]})
# "abc".to(Array).map |$i,$v| { [$i, $v]}.to(Hash)
# ```
#
# @since Puppet 5.0.0
#
Puppet::Functions.create_function(:unique) do
  dispatch :unique_string do
    param 'String', :string
    optional_block_param 'Callable[String]', :block
  end

  dispatch :unique_hash do
    param 'Hash', :hash
    optional_block_param 'Callable[Any]', :block
  end

  dispatch :unique_array do
    param 'Array', :array
    optional_block_param 'Callable[Any]', :block
  end

  dispatch :unique_iterable do
    param 'Iterable', :iterable
    optional_block_param 'Callable[Any]', :block
  end

  def unique_string(string, &block)
    string.split('').uniq(&block).join('')
  end

  def unique_hash(hash, &block)
    block = ->(v) { v } unless block_given?
    result = Hash.new { |h, k| h[k] = { :keys => [], :values => [] } }
    hash.each_pair do |k, v|
      rc = result[block.call(v)]
      rc[:keys] << k
      rc[:values] << v
    end
    # reduce the set of possibly duplicated value entries
    inverted = {}
    result.each_pair { |_k, v| inverted[v[:keys]] = v[:values].uniq }
    inverted
  end

  def unique_array(array, &block)
    array.uniq(&block)
  end

  def unique_iterable(iterable, &block)
    Puppet::Pops::Types::Iterable.on(iterable).uniq(&block)
  end
end
