module RubySL
  module Socket
    def self.bsd_support?
      Rubinius.bsd? || Rubinius.darwin?
    end

    def self.linux_support?
      Rubinius.linux?
    end

    def self.unix_socket_support?
      ::Socket::Constants.const_defined?(:AF_UNIX)
    end

    def self.aliases_for_hostname(hostname)
      pointer = Foreign.gethostbyname(hostname)

      Foreign::Hostent.new(pointer).aliases
    end

    def self.sockaddr_class_for_socket(socket)
      if socket.is_a?(::UNIXSocket)
        return Foreign::SockaddrUn
      end

      # Socket created using for example Socket.unix('foo')
      if socket.is_a?(::Socket) and
        socket.instance_variable_get(:@family) == ::Socket::AF_UNIX
        return Foreign::SockaddrUn
      end

      case address_info(:getsockname, socket)[0]
      when 'AF_INET6'
        Foreign::SockaddrIn6
      when 'AF_UNIX'
        Foreign::SockaddrUn
      else
        Foreign::SockaddrIn
      end
    end

    def self.accept(source, new_class)
      raise IOError, 'socket has been closed' if source.closed?

      sockaddr = sockaddr_class_for_socket(source).new

      begin
        fd = RubySL::Socket::Foreign.memory_pointer(:int) do |size_p|
          size_p.write_int(sockaddr.size)

          RubySL::Socket::Foreign
            .accept(source.descriptor, sockaddr.pointer, size_p)
        end

        Error.read_error('accept(2)', source) if fd < 0

        socket = new_class.allocate

        ::IO.setup(socket, fd, nil, true)
        socket.binmode

        socktype = source.getsockopt(:SOCKET, :TYPE).int
        addrinfo = Addrinfo.new(sockaddr.to_s, sockaddr.family, socktype)

        return socket, addrinfo
      ensure
        sockaddr.free
      end
    end

    def self.accept_nonblock(source, new_class)
      source.fcntl(::Fcntl::F_SETFL, ::Fcntl::O_NONBLOCK)

      accept(source, new_class)
    end

    def self.listen(source, backlog)
      backlog = Rubinius::Type.coerce_to(backlog, Fixnum, :to_int)
      err     = Foreign.listen(source.descriptor, backlog)

      Error.read_error('listen(2)', source) if err < 0

      0
    end

    def self.family_for_sockaddr_in(sockaddr)
      case sockaddr.bytesize
      when 28
        ::Socket::AF_INET6
      when 16
        ::Socket::AF_INET
      # UNIX socket addresses can have a variable size as sometimes any trailing
      # null bytes are stripped (e.g. when calling UNIXServer#getsockname).
      else
        ::Socket::AF_UNIX
      end
    end

    def self.constant_pairs
      Rubinius::FFI.config_hash('socket').reject { |name, value| value.empty? }
    end

    def self.coerce_to_string(object)
      if object.is_a?(String) or object.is_a?(Symbol)
        object.to_s
      elsif object.respond_to?(:to_str)
        Rubinius::Type.coerce_to(object, String, :to_str)
      else
        raise TypeError, "no implicit conversion of #{object.inspect} into Integer"
      end
    end

    def self.family_prefix?(family)
      family.start_with?('AF_') || family.start_with?('PF_')
    end

    def self.prefix_with(name, prefix)
      unless name.start_with?(prefix)
        name = "#{prefix}#{name}"
      end

      name
    end

    def self.prefixed_socket_constant(name, prefix, &block)
      prefixed = prefix_with(name, prefix)

      socket_constant(prefixed, &block)
    end

    def self.socket_constant(name)
      if ::Socket.const_defined?(name)
        ::Socket.const_get(name)
      else
        raise SocketError, yield
      end
    end

    def self.address_family(family)
      case family
      when Symbol, String
        f = family.to_s

        unless family_prefix?(f)
          f = 'AF_' + f
        end

        if ::Socket.const_defined?(f)
          ::Socket.const_get(f)
        else
          raise SocketError, "unknown socket domain: #{family}"
        end
      when Integer
        family
      when NilClass
        ::Socket::AF_UNSPEC
      else
        if family.respond_to?(:to_str)
          address_family(Rubinius::Type.coerce_to(family, String, :to_str))
        else
          raise SocketError, "unknown socket domain: #{family}"
        end
      end
    end

    def self.address_family_name(family_int)
      # Both AF_LOCAL and AF_UNIX use value 1. CRuby seems to prefer AF_UNIX
      # over AF_LOCAL.
      if family_int == ::Socket::AF_UNIX && family_int == ::Socket::AF_LOCAL
        return 'AF_UNIX'
      end

      ::Socket.constants.grep(/^AF_/).each do |name|
        return name.to_s if ::Socket.const_get(name) == family_int
      end

      'AF_UNSPEC'
    end

    def self.protocol_family_name(family_int)
      # Both PF_LOCAL and PF_UNIX use value 1. CRuby seems to prefer PF_UNIX
      # over PF_LOCAL.
      if family_int == ::Socket::PF_UNIX && family_int == ::Socket::PF_LOCAL
        return 'PF_UNIX'
      end

      ::Socket.constants.grep(/^PF_/).each do |name|
        return name.to_s if ::Socket.const_get(name) == family_int
      end

      'PF_UNSPEC'
    end

    def self.protocol_name(family_int)
      ::Socket.constants.grep(/^IPPROTO_/).each do |name|
        return name.to_s if ::Socket.const_get(name) == family_int
      end

      'IPPROTO_IP'
    end

    def self.socket_type_name(socktype)
      ::Socket.constants.grep(/^SOCK_/).each do |name|
        return name.to_s if ::Socket.const_get(name) == socktype
      end

      nil
    end

    def self.protocol_family(family)
      case family
      when Symbol, String
        f = family.to_s

        unless family_prefix?(f)
          f = 'PF_' + f
        end

        if ::Socket.const_defined?(f)
          ::Socket.const_get(f)
        else
          raise SocketError, "unknown socket domain: #{family}"
        end
      when Integer
        family
      when NilClass
        ::Socket::PF_UNSPEC
      else
        if family.respond_to?(:to_str)
          protocol_family(Rubinius::Type.coerce_to(family, String, :to_str))
        else
          raise SocketError, "unknown socket domain: #{family}"
        end
      end
    end

    def self.socket_type(type)
      case type
      when Symbol, String
        t = type.to_s

        if t[0..4] != 'SOCK_'
          t = "SOCK_#{t}"
        end

        if ::Socket.const_defined?(t)
          ::Socket.const_get(t)
        else
          raise SocketError, "unknown socket type: #{type}"
        end
      when Integer
        type
      when NilClass
        0
      else
        if type.respond_to?(:to_str)
          socket_type(Rubinius::Type.coerce_to(type, String, :to_str))
        else
          raise SocketError, "unknown socket type: #{type}"
        end
      end
    end

    def self.convert_reverse_lookup(socket = nil, reverse_lookup = nil)
      if reverse_lookup.nil?
        if socket
          reverse_lookup = !socket.do_not_reverse_lookup
        else
          reverse_lookup = !BasicSocket.do_not_reverse_lookup
        end

      elsif reverse_lookup == :hostname
        reverse_lookup = true

      elsif reverse_lookup == :numeric
        reverse_lookup = false

      elsif reverse_lookup != true and reverse_lookup != false
        raise ArgumentError,
          "invalid reverse_lookup flag: #{reverse_lookup.inspect}"
      end

      reverse_lookup
    end

    def self.address_info(method, socket, reverse_lookup = nil)
      sockaddr = Foreign.__send__(method, socket.descriptor)

      reverse_lookup = convert_reverse_lookup(socket, reverse_lookup)

      options = ::Socket::Constants::NI_NUMERICHOST |
        ::Socket::Constants::NI_NUMERICSERV

      family, port, host, ip = Foreign
        .getnameinfo(sockaddr, options, reverse_lookup)

      [family, port.to_i, host, ip]
    end

    def self.shutdown_option(how)
      case how
      when String, Symbol
        prefixed_socket_constant(how.to_s, 'SHUT_') do
          "unknown shutdown argument: #{how}"
        end
      when Fixnum
        if how == ::Socket::SHUT_RD or
          how == ::Socket::SHUT_WR or
          how == ::Socket::SHUT_RDWR
          how
        else
          raise ArgumentError,
            'argument should be :SHUT_RD, :SHUT_WR, or :SHUT_RDWR'
        end
      else
        if how.respond_to?(:to_str)
          shutdown_option(coerce_to_string(how))
        else
          raise TypeError,
            "no implicit conversion of #{how.class} into Integer"
        end
      end
    end
  end
end
