module ChildProcess
  module Windows
    class ProcessBuilder
      attr_accessor :detach, :duplex, :environment, :stdout, :stderr, :cwd
      attr_reader :stdin

      def initialize(args)
        @args        = args

        @detach      = false
        @duplex      = false
        @environment = nil
        @cwd         = nil

        @stdout      = nil
        @stderr      = nil
        @stdin       = nil

        @flags       = 0
        @job_ptr     = nil
        @cmd_ptr     = nil
        @env_ptr     = nil
        @cwd_ptr     = nil
      end

      def start
        create_command_pointer
        create_environment_pointer
        create_cwd_pointer

        setup_flags
        setup_io

        pid = create_process
        close_handles

        pid
      end

      private

      def create_command_pointer
        string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join ' '
        @cmd_ptr = FFI::MemoryPointer.from_string string
      end

      def create_environment_pointer
        return unless @environment.kind_of?(Hash) && @environment.any?

        strings = []

        ENV.to_hash.merge(@environment).each do |key, val|
          next if val.nil?

          if key.to_s =~ /=|\0/ || val.to_s.include?("\0")
            raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.inspect}"
          end

          strings << "#{key}=#{val}\0"
        end

        strings << "\0" # terminate the env block
        env_str = strings.join

        @env_ptr = FFI::MemoryPointer.new(:long, env_str.bytesize)
        @env_ptr.put_bytes 0, env_str, 0, env_str.bytesize
      end

      def create_cwd_pointer
        @cwd_ptr = FFI::MemoryPointer.from_string(@cwd || Dir.pwd)
      end

      def create_process
        ok = Lib.create_process(
          nil,          # application name
          @cmd_ptr,     # command line
          nil,          # process attributes
          nil,          # thread attributes
          true,         # inherit handles
          @flags,       # creation flags
          @env_ptr,     # environment
          @cwd_ptr,     # current directory
          startup_info, # startup info
          process_info  # process info
        )

        ok or raise LaunchError, Lib.last_error_message

        process_info[:dwProcessId]
      end

      def startup_info
        @startup_info ||= StartupInfo.new
      end

      def process_info
        @process_info ||= ProcessInfo.new
      end

      def setup_flags
        @flags |= DETACHED_PROCESS if @detach
      end

      def setup_io
        startup_info[:dwFlags] ||= 0
        startup_info[:dwFlags] |= STARTF_USESTDHANDLES

        if @stdout
          startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
        end

        if @stderr
          startup_info[:hStdError] = std_stream_handle_for(@stderr)
        end

        if @duplex
          read_pipe_ptr  = FFI::MemoryPointer.new(:pointer)
          write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
          sa             = SecurityAttributes.new(:inherit => true)

          ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
          Lib.check_error ok

          @read_pipe  = read_pipe_ptr.read_pointer
          @write_pipe = write_pipe_ptr.read_pointer

          Lib.set_handle_inheritance @read_pipe, true
          Lib.set_handle_inheritance @write_pipe, false

          startup_info[:hStdInput] = @read_pipe
        else
          startup_info[:hStdInput] = std_stream_handle_for(STDIN)
        end
      end

      def std_stream_handle_for(io)
        handle = Lib.handle_for(io)

        begin
          Lib.set_handle_inheritance handle, true
        rescue ChildProcess::Error
          # If the IO was set to close on exec previously, this call will fail.
          # That's probably OK, since the user explicitly asked for it to be
          # closed (at least I have yet to find other cases where this will
          # happen...)
        end

        handle
      end

      def close_handles
        Lib.close_handle process_info[:hProcess]
        Lib.close_handle process_info[:hThread]

        if @duplex
          @stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
          Lib.close_handle @read_pipe
          Lib.close_handle @write_pipe
        end
      end

      def quote_if_necessary(str)
        quote = str.start_with?('"') ? "'" : '"'

        case str
        when /[\s\\'"]/
          [quote, str, quote].join
        else
          str
        end
      end
    end # ProcessBuilder
  end # Windows
end # ChildProcess
