#
# Custom implementation of Open3.popen{3,2,2e} that uses java.lang.ProcessBuilder rather than pipes and spawns.
#

require 'jruby' # need access to runtime for RubyStatus construction

module Open3

  java_import java.lang.ProcessBuilder
  java_import org.jruby.RubyProcess
  java_import org.jruby.util.ShellLauncher

  def popen3(*cmd, &block)
    if cmd.size > 0 && Hash === cmd[-1]
      opts = cmd.pop
    else
      opts = {}
    end
    processbuilder_run(cmd, opts, io: IO_3, &block)
  end
  module_function :popen3

  IO_3 = proc do |process|
    [process.getOutputStream.to_io, process.getInputStream.to_io, process.getErrorStream.to_io]
  end

  BUILD_2 = proc do |builder|
    builder.redirectError(ProcessBuilder::Redirect::INHERIT)
  end

  IO_2 = proc do |process|
    [process.getOutputStream.to_io, process.getInputStream.to_io]
  end

  def popen2(*cmd, &block)
    if cmd.size > 0 && Hash === cmd[-1]
      opts = cmd.pop
    else
      opts = {}
    end
    processbuilder_run(cmd, opts, build: BUILD_2, io: IO_2, &block)
  end
  module_function :popen2

  BUILD_2E = proc do |builder|
    builder.redirectErrorStream(true)
  end

  def popen2e(*cmd, &block)
    if cmd.size > 0 && Hash === cmd[-1]
      opts = cmd.pop
    else
      opts = {}
    end
    processbuilder_run(cmd, opts, build: BUILD_2E, io: IO_2, &block)
  end
  module_function :popen2e

  def processbuilder_run(cmd, opts, build: nil, io:)
    opts.each do |k, v|
      if Integer === k
        if IO == v || !(String === v || v.respond_to?(:to_path))
          # target is an open IO or a non-pathable object, bail out
          raise NotImplementedError.new("redirect to an open IO is not implemented on this platform")
        end
      end
    end

    if Hash === cmd[0]
      env = cmd.shift;
    else
      env = {}
    end

    if cmd.size == 1 && (cmd[0] =~ / / || ShellLauncher.shouldUseShell(cmd[0]))
      cmd = [RbConfig::CONFIG['SHELL'], JRuby::Util::ON_WINDOWS ? '/c' : '-c', cmd[0]]
    end

    builder = ProcessBuilder.new(cmd.to_java(:string))

    builder.directory(java.io.File.new(opts[:chdir] || Dir.pwd))

    environment = builder.environment
    env.each { |k, v| v.nil? ? environment.remove(k) : environment.put(k, v) }

    build.call(builder) if build

    process = builder.start

    pid = org.jruby.util.ShellLauncher.getPidFromProcess(process)

    parent_io = io.call(process)

    parent_io.each {|i| i.sync = true}

    wait_thr = DetachThread.new(pid) { RubyProcess::RubyStatus.newProcessStatus(JRuby.runtime, process.waitFor << 8, pid) }

    result = [*parent_io, wait_thr]

    if defined? yield
      begin
        return yield(*result)
      ensure
        parent_io.each(&:close)
        wait_thr.join
      end
    end

    result
  end
  module_function :processbuilder_run
  class << self
    private :processbuilder_run
  end

  class DetachThread < Thread
    attr_reader :pid

    def initialize(pid)
      super

      @pid = pid
      self[:pid] = pid
    end
  end

end
