#!/usr/bin/ruby

#
## https://rosettacode.org/wiki/Free_polyominoes_enumeration
#

func translate2origin(poly) {
  # Finds the min x and y coordiate of a Polyomino.
  var minx = poly.map(:head).min
  var miny = poly.map(:tail).min
  poly.map_2d {|x,y| [x-minx, y-miny] }.sort
}

func rotate90(x,y) { [y, -x] }
func  reflect(x,y) { [-x, y] }

# All the plane symmetries of a rectangular region.
func rotations_and_reflections(poly) {
    gather {
        take(poly)
        take(poly.map_2d!{|x,y| rotate90(x,y) })
        take(poly.map_2d!{|x,y| rotate90(x,y) })
        take(poly.map_2d!{|x,y| rotate90(x,y) })
        take(poly.map_2d!{|x,y|  reflect(x,y) })
        take(poly.map_2d!{|x,y| rotate90(x,y) })
        take(poly.map_2d!{|x,y| rotate90(x,y) })
        take(poly.map_2d!{|x,y| rotate90(x,y) })
    }
}

func canonical(poly) {
  rotations_and_reflections(poly).map(translate2origin)
}

# All four points in Von Neumann neighborhood.
func contiguous(x, y) {
  [[x-1, y], [x+1, y], [x, y-1], [x, y+1]]
}

# Finds all distinct points that can be added to a Polyomino.
func new_points(poly) {
  var points = Set()
  poly.each_2d {|x,y| points << contiguous(x,y)... }
  points - poly
}

func new_polys(polys) {
  var pattern = Bag()
  polys.map { |poly|
    gather {
      new_points(poly).each_2d { |x,y|
        var pl = translate2origin(poly + [[x,y]])
        next if pattern.has(pl)
        take(var t = canonical(pl) -> min)
        pattern << t...
      }
    }...
  }
}

# Generates polyominoes of rank n recursively.
func rank(n) {
  given (n) {
    when (0) { [[]] }
    when (1) { [[[0,0]]] }
    else     { new_polys(rank(n-1)) }
  }
}

# Generates a textual representation of a Polyomino.
func text_representation(poly) {
  var table = Hash()
  poly.each_2d {|x,y| table{[x,y]} = '#' }
  var maxx = poly.map(:head).max
  var maxy = poly.map(:tail).max
  (0..maxx).map{|x| (0..maxy).map{|y| table{[x,y]} \\ ' ' }.join }
}

var arr = 5.of { rank(_).len }

say arr
assert_eq(arr, [1, 1, 1, 2, 5])

var n = (ARGV[0] ? ARGV[0].to_i : 4)
say ("\nAll free polyominoes of rank %d:" % n)
rank(n).sort.each{|poly| say text_representation(poly).join("\n")+"\n" }
