require "set"

require "minitest"
require "minitest/pride"

class Slice
  def self.parse(s)
    squares = {}
    s.scan(/^(x|y)=(\d+), (?:x|y)=(\d+)..(\d+)$/m).each do |xy, i, min, max|
      i, min, max = [i, min, max].map(&:to_i)
      (min..max).map {|j| xy == ?x ? [i, j] : [j, i] }.each do |pos|
        squares[pos] = ?#
      end
    end
    new(squares)
  end

  attr_reader :squares

  def initialize(squares)
    @squares = squares
    @squares[[500, 0]] = ?+
    @active = Set[[500, 0]]
  end

  def simulate
    return enum_for(__method__) unless block_given?

    max_y = @squares.keys.map(&:last).max

    loop do
      @active.to_a.each do |pos|
        @active.delete(pos)
        x, y = pos
        case @squares.fetch(pos)
        when ?+
          @squares[[x, y+1]] = ?|
          @active << [x, y+1]
        when ?|
          next if y+1 > max_y

          if @squares[[x, y+1]].nil?
            @squares[[x, y+1]] = ?|
            @active << [x, y+1]
          else
            if @squares[[x-1, y]].nil?
              @squares[[x-1, y]] = ?|
              @active << [x-1, y]
            end

            if @squares[[x+1, y]].nil?
              @squares[[x+1, y]] = ?|
              @active << [x+1, y]
            end

            left_x = x-1
            left_x -= 1 until @squares[[left_x, y]] != ?|
            right_x = x+1
            right_x += 1 until @squares[[right_x, y]] != ?|
            if @squares[[left_x, y]] == ?# && @squares[[right_x, y]] == ?#
              (left_x+1..right_x-1).each do |x|
                @squares[[x, y]] = ?~
                @active << [x, y-1] if @squares[[x, y-1]] == ?|
              end
            end
          end
        end
      end

      return if @active.empty?

      yield self
    end
  end

  def water
    @squares.select {|_, s| %w[ | ~ ].include?(s) }
  end

  def to_s
    min_x, max_x = @squares.keys.map(&:first).minmax
    min_y, max_y = @squares.keys.map(&:last).minmax
    min_y = [0, min_y].min
    (min_y..max_y).map {|y| (min_x-1..max_x+1).map {|x| @squares.fetch([x, y]) { ?. } }.join }.join(?\n)
  end
end

class TestSlice < Minitest::Test
  def setup
    @slice = Slice.parse(<<~SLICE)
      x=495, y=2..7
      y=7, x=495..501
      x=501, y=3..7
      x=498, y=2..4
      x=506, y=1..2
      x=498, y=10..13
      x=504, y=10..13
      y=13, x=498..504
    SLICE
  end

  def test_parse_to_s
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ............#.
      .#..#.......#.
      .#..#..#......
      .#..#..#......
      .#.....#......
      .#.....#......
      .#######......
      ..............
      ..............
      ....#.....#...
      ....#.....#...
      ....#.....#...
      ....#######...
    SLICE
  end

  def test_simulate
    simulation = @slice.simulate

    5.times { simulation.next }
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ......|.....#.
      .#..#.|.....#.
      .#..#.|#......
      .#..#.|#......
      .#....|#......
      .#.....#......
      .#######......
      ..............
      ..............
      ....#.....#...
      ....#.....#...
      ....#.....#...
      ....#######...
    SLICE

    5.times { simulation.next }
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ......|.....#.
      .#..#.|.....#.
      .#..#.|#......
      .#..#.|#......
      .#....|#......
      .#~~~~~#......
      .#######......
      ..............
      ..............
      ....#.....#...
      ....#.....#...
      ....#.....#...
      ....#######...
    SLICE

    4.times { simulation.next }
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ......|.....#.
      .#..#.|.....#.
      .#..#.|#......
      .#..#.|#......
      .#~~~~~#......
      .#~~~~~#......
      .#######......
      ..............
      ..............
      ....#.....#...
      ....#.....#...
      ....#.....#...
      ....#######...
    SLICE

    2.times { simulation.next }
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ......|.....#.
      .#..#.|.....#.
      .#..#~~#......
      .#..#~~#......
      .#~~~~~#......
      .#~~~~~#......
      .#######......
      ..............
      ..............
      ....#.....#...
      ....#.....#...
      ....#.....#...
      ....#######...
    SLICE

    21.times { simulation.next }
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ......|.....#.
      .#..#||||...#.
      .#..#~~#|.....
      .#..#~~#|.....
      .#~~~~~#|.....
      .#~~~~~#|.....
      .#######|.....
      ........|.....
      ........|.....
      ....#~~~~~#...
      ....#~~~~~#...
      ....#~~~~~#...
      ....#######...
    SLICE

    9.times { simulation.next }
    assert_equal <<~SLICE.chomp, @slice.to_s
      ......+.......
      ......|.....#.
      .#..#||||...#.
      .#..#~~#|.....
      .#..#~~#|.....
      .#~~~~~#|.....
      .#~~~~~#|.....
      .#######|.....
      ........|.....
      ...|||||||||..
      ...|#~~~~~#|..
      ...|#~~~~~#|..
      ...|#~~~~~#|..
      ...|#######|..
    SLICE

    assert_equal 57, @slice.water.count
  end
end

def solve(input)
  slice = Slice.parse(input)
  min_y = slice.squares.select {|_,s| s == ?# }.map(&:first).map(&:last).min
  simulation = slice.simulate

  loop do
    simulation.next
  end

  # puts
  # min_x, max_x = slice.water.map(&:first).minmax
  # min_y, max_y = slice.water.map(&:last).minmax
  # min_y = [0, min_y].min
  # puts (min_y..max_y).map {|y| (min_x-1..max_x+1).map {|x| slice.squares.fetch([x, y]) { ?. } }.join }.join(?\n)

  # Part One
  # slice.water.count {|(_,y),_| y >= min_y }

  slice.water.count {|_,s| s == ?~ }
end

if __FILE__ == $0
  require "minitest/autorun" and exit if ENV["TEST"]

  puts solve(ARGF.read)
end