class ParallelRunner
  def initialize(files, processes, formatter, argv)
    @files = files
    @processes = processes
    @formatter = formatter
    @argv = argv
    @last_files = {}
    @output_files = []
    @success = true
  end

  def launch_children
    @children = @processes.times.map { |i|
      name = tmp "mspec-multi-#{i}"
      @output_files << name

      env = {
        "SPEC_TEMP_DIR" => "rubyspec_temp_#{i}",
        "MSPEC_MULTI" => i.to_s
      }
      command = @argv + ["-fy", "-o", name]
      $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
      IO.popen([env, *command, close_others: false], "rb+")
    }
  end

  def handle(child, message)
    case message
    when '.'
      @formatter.unload
      send_new_file_or_quit(child)
    else
      if message == nil
        msg = "A child mspec-run process died unexpectedly"
      else
        msg = "A child mspec-run process printed unexpected output on STDOUT"
        while chunk = (child.read_nonblock(4096) rescue nil)
          message += chunk
        end
        message.chomp!('.')
        msg += ": #{message.inspect}"
      end

      if last_file = @last_files[child]
        msg += " while running #{last_file}"
      end

      @success = false
      quit(child)
      abort "\n#{msg}"
    end
  end

  def quit(child)
    begin
      child.puts "QUIT"
    rescue Errno::EPIPE
      # The child process already died
    end
    _pid, status = Process.wait2(child.pid)
    @success &&= status.success?
    child.close
    @children.delete(child)
  end

  def send_new_file_or_quit(child)
    if @files.empty?
      quit(child)
    else
      file = @files.shift
      @last_files[child] = file
      child.puts file
    end
  end

  def run
    MSpec.register_files @files
    launch_children

    puts @children.map { |child| child.gets }.uniq
    @formatter.start
    begin
      @children.each { |child| send_new_file_or_quit(child) }

      until @children.empty?
        IO.select(@children)[0].each { |child|
          handle(child, child.read(1))
        }
      end
    ensure
      @children.dup.each { |child| quit(child) }
      @formatter.aggregate_results(@output_files)
      @formatter.finish
    end

    @success
  end
end
