require 'ffi'
require 'thread'

module ChildProcess
  module Unix
    class PosixSpawnProcess < Process
      private

      @@cwd_lock = Mutex.new

      def launch_process
        pid_ptr = FFI::MemoryPointer.new(:pid_t)
        actions = Lib::FileActions.new
        attrs   = Lib::Attrs.new

        if io.stdout
          actions.add_dup fileno_for(io.stdout), fileno_for(STDOUT)
        else
          actions.add_open fileno_for(STDOUT), "/dev/null", File::WRONLY, 0644
        end

        if io.stderr
          actions.add_dup fileno_for(io.stderr), fileno_for(STDERR)
        else
          actions.add_open fileno_for(STDERR), "/dev/null", File::WRONLY, 0644
        end

        if duplex?
          reader, writer = ::IO.pipe
          actions.add_dup fileno_for(reader), fileno_for(STDIN)
          actions.add_close fileno_for(writer)
        end

        attrs.pgroup = 0 if leader?
        attrs.flags |= Platform::POSIX_SPAWN_USEVFORK if defined? Platform::POSIX_SPAWN_USEVFORK

        # wrap in helper classes in order to avoid GC'ed pointers
        argv = Argv.new(@args)
        envp = Envp.new(ENV.to_hash.merge(@environment))

        ret = 0
        @@cwd_lock.synchronize do
          Dir.chdir(@cwd || Dir.pwd) do
            if ChildProcess.jruby?
              # on JRuby, the current working directory is for some reason not inherited.
              # We'll work around it by making a chdir call through FFI.
              # TODO: report this to JRuby
              Lib.chdir Dir.pwd
            end

            ret = Lib.posix_spawnp(
              pid_ptr,
              @args.first, # TODO: not sure this matches exec() behaviour
              actions,
              attrs,
              argv,
              envp
            )
          end
        end

        if duplex?
          io._stdin = writer
          reader.close
        end

        actions.free
        attrs.free

        if ret != 0
          raise LaunchError, "#{Lib.strerror(ret)} (#{ret})"
        end

        @pid = pid_ptr.read_int
        ::Process.detach(@pid) if detach?
      end

      if ChildProcess.jruby?
        def fileno_for(obj)
          ChildProcess::JRuby.posix_fileno_for(obj)
        end
      else
        def fileno_for(obj)
          obj.fileno
        end
      end

      class Argv
        def initialize(args)
          @ptrs = args.map do |e|
            if e.include?("\0")
              raise ArgumentError, "argument cannot contain null bytes: #{e.inspect}"
            end

            FFI::MemoryPointer.from_string(e.to_s)
          end

          @ptrs << FFI::Pointer.new(0)
        end

        def to_ptr
          argv = FFI::MemoryPointer.new(:pointer, @ptrs.size)
          argv.put_array_of_pointer(0, @ptrs)

          argv
        end
      end # Argv

      class Envp
        def initialize(env)
          @ptrs = env.map do |key, val|
            next if val.nil?

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

            FFI::MemoryPointer.from_string("#{key}=#{val.to_s}")
          end.compact

          @ptrs << FFI::Pointer.new(0)
        end

        def to_ptr
          env = FFI::MemoryPointer.new(:pointer, @ptrs.size)
          env.put_array_of_pointer(0, @ptrs)

          env
        end
      end # Envp

    end
  end
end
