require 'facets/functor'

module Enumerable

  # TODO: Should Enumerable#accumuate be moved to a functor gem?"

  # Accumulate a set of a set. For example, in an ORM design
  # where `Group has_many User` we might have something
  # equivalent to the following.
  #
  #     Group = Struct.new(:users)
  #     User  = Struct.new(:name, :friends)
  #
  #     user1 = User.new('John', [])
  #     user2 = User.new('Jane', ['Jill'])
  #     user3 = User.new('Joe' , ['Jack', 'Jim'])
  #
  #     group1 = Group.new([user1, user2])
  #     group2 = Group.new([user2, user3])
  #
  #     groups = [group1, group2]
  #
  # Now we can *accumulate* the users of all groups.
  #
  #     groups.accumulate.users  #=> [user1, user2, user3]
  #
  # You may pass an argument to perform chains, e.g. the following
  # returns the names of users from all groups.
  #
  #     groups.accumulate(2).users.name  #=> ['John','Jane','Joe']
  #
  # Or we can gather all the friends of all users in groups.
  #
  #     groups.accumulate(2).users.friends  #=> ['Jill','Jack','Jim']
  # 
  # This is more convenient then the equivalent.
  #
  #     groups.accumulate.users.accumulate.friends  #=> ['Jill','Jack','Jim']
  #
  # CREDIT: George Moshchovitis, Daniel Emirikol, Robert Dober

  def accumulate(iterations=1)
    return self if iterations == 0

    Functor.new do |op, *args|
      result = []
      each { |x| result << x.send(op, *args) }
      result.flatten.uniq.accumulate(iterations - 1)
    end
  end

  # Same as #accumulate, but does not apply #uniq to final result.
  #
  #     groups.accumulate_all(2).users.friends  #=> ['Jill', 'Jill','Jack','Jim']
  #
  def accumulate_all(iterations=1)
    return self if iterations == 0

    Functor.new do |op, *args|
      result = []
      each { |x| result << x.send(op, *args) }
      result.flatten.accumulate_all(iterations - 1)
    end
  end

end

