input = ARGF.readlines(chomp: true).map {|row|
  springs, groups = row.split(" ")
  [springs, groups.scan(/\d+/).map(&:to_i)]
}

p input
  # .map { [ _1.chars, _2 ] }
  .map { [Array.new(5, _1).join(??).chars, _2 * 5] }
  .map {|springs, groups|
    arrangement_counts = Hash.new {|h,k|
      count, springs, groups = k
      first, *rest = springs
      h[k] = case [count, first, groups]
             in count, _, []
               if count.nonzero?
                 0
               elsif springs.include?(?#)
                 0
               else
                 1
               end
             in count, _, groups if count > groups.fetch(0)
               0 # early exit if the damaged group ever gets too large
             in count, nil, groups # no more springs
               if groups.size > 1
                 0
               elsif count == groups.fetch(0)
                 1
               else
                 0
               end
             in count, ?#, groups # continue a damaged group
                 h[[count + 1, rest, groups]]
             in 0, ?., groups
               h[[0, rest, groups]]
             in count, ?., groups # finish a damaged group
               if count == groups.fetch(0)
                 h[[0, rest, groups[1..]]]
               else
                 0
               end
             in count, ??, g if count == g.fetch(0) # unknown spring, matched current group
               h[[0, rest, g[1..]]]
             in count, ??, g unless count.zero? # unknown spring, ongoing group
               h[[count + 1, rest, g]]
             in count, ??, g # unknown spring
               [
                 h[[count, rest, g]], # undamaged
                 h[[count + 1, rest, g]], # damaged
               ].sum
             else
               fail "#{{first:, springs: springs.join, groups:}}"
             end
    }
    arrangement_counts[[0, springs, groups]]
  }.sum

# original part one
# p input.map {|springs, groups|
#   unknowns = springs.count(??)
#   (0...2**unknowns).map {|arrangement|
#     arrangement = arrangement.to_s(2).rjust(unknowns, ?0).chars.map { _1 == ?0 ? ?. : ?# }
#     candidate = springs.gsub(??) { arrangement.shift }
#     candidate.chars
#       .slice_when { _1 != _2 }
#       .reject { _1[0] == ?. }
#       .map(&:size) == groups
#   }.count(&:itself)
# }.sum