#--
# Copyright (C)2007 Tony Arcieri
# You can redistribute this under the terms of the Ruby license
# See file LICENSE for details
#++

require 'socket'
require 'resolv'

module Coolio
  class Socket < IO
    def self.connect(socket, *args)

      new(socket, *args).instance_eval do
        @_connector = Connector.new(self, socket)
        self
      end
    end

    # Just initializes some instance variables to avoid
    # warnings and calls super().
    def initialize *args
      @_failed = nil
      @_connector = nil
      super
    end

    watcher_delegate :@_connector

    remove_method :attach
    def attach(evloop)
      raise RuntimeError, "connection failed" if @_failed

      if @_connector
        @_connector.attach(evloop)
        return self
      end

      super
    end

    # Called upon completion of a socket connection
    def on_connect; end
    event_callback :on_connect

    # Called if a socket connection failed to complete
    def on_connect_failed; end
    event_callback :on_connect_failed

    # Called if a hostname failed to resolve when connecting
    # Defaults to calling on_connect_failed
    alias_method :on_resolve_failed, :on_connect_failed

    #########
    protected
    #########

    class Connector < IOWatcher
      def initialize(coolio_socket, ruby_socket)
        @coolio_socket, @ruby_socket = coolio_socket, ruby_socket
        super(ruby_socket, :w)
      end

      def on_writable
        evl = evloop
        detach

        if connect_successful?
          @coolio_socket.instance_eval { @_connector = nil }
          @coolio_socket.attach(evl)
          @ruby_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, [1].pack("l"))
          @ruby_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)

          @coolio_socket.__send__(:on_connect)
        else
          @coolio_socket.instance_eval { @_failed = true }
          @coolio_socket.__send__(:on_connect_failed)
        end
      end

      #######
      private
      #######

      def connect_successful?
        @ruby_socket.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_ERROR).unpack('i').first == 0
      rescue IOError
        false
      end
    end
  end

  class TCPSocket < Socket
    attr_reader :remote_host, :remote_addr, :remote_port, :address_family
    watcher_delegate :@_resolver

    # Similar to .new, but used in cases where the resulting object is in a
    # "half-open" state.  This is primarily used for when asynchronous
    # DNS resolution is taking place.  We don't actually have a handle to
    # the socket we want to use to create the watcher yet, since we don't
    # know the IP address to connect to.
    def self.precreate(*args, &block)
      obj = allocate
      obj.__send__(:preinitialize, *args, &block)
      obj
    end

    # Perform a non-blocking connect to the given host and port
    # see examples/echo_client.rb
    # addr is a string, can be an IP address or a hostname.
    def self.connect(addr, port, *args)
      family = nil

      if (Resolv::IPv4.create(addr) rescue nil)
        family = ::Socket::AF_INET
      elsif(Resolv::IPv6.create(addr) rescue nil)
        family = ::Socket::AF_INET6
      end

      if family
        return super(TCPConnectSocket.new(family, addr, port), *args) # this creates a 'real' write buffer so we're ok there with regards to already having a write buffer from the get go
      end

      if host = Coolio::DNSResolver.hosts(addr)
        return connect(host, port, *args) # calls this same function
      end

      precreate(addr, port, *args)
    end

    # Called by precreate during asyncronous DNS resolution
    def preinitialize(addr, port, *args)
      @_write_buffer = ::Coolio::Buffer.new # allow for writing BEFORE DNS has resolved
      @remote_host, @remote_addr, @remote_port = addr, addr, port
      @_resolver = TCPConnectResolver.new(self, addr, port, *args)
    end

    private :preinitialize

    PEERADDR_FAILED = ["?", 0, "name resolusion failed", "?"]

    def initialize(socket)
      unless socket.is_a?(::TCPSocket) or socket.is_a?(TCPConnectSocket)
        raise TypeError, "socket must be a TCPSocket"
      end

      super

      @address_family, @remote_port, @remote_host, @remote_addr = (socket.peeraddr rescue PEERADDR_FAILED)
    end

    def peeraddr
      [@address_family, @remote_port, @remote_host, @remote_addr]
    end

    #########
    protected
    #########

    class TCPConnectSocket < ::Socket
      def initialize(family, addr, port, host = addr)
        @host, @addr, @port = host, addr, port
        @address_family = nil

        super(family, ::Socket::SOCK_STREAM, 0)
        begin
          connect_nonblock(::Socket.sockaddr_in(port, addr))
        rescue Errno::EINPROGRESS
        end
      end

      def peeraddr
        [
          @address_family == ::Socket::AF_INET ? 'AF_INET' : 'AF_INET6',
          @port,
          @host,
          @addr
        ]
      end
    end

    class TCPConnectResolver < Coolio::DNSResolver
      def initialize(socket, host, port, *args)
        @sock, @host, @port, @args = socket, host, port, args
        super(host)
      end

      def on_success(addr)
        host, port, args = @host, @port, @args

        @sock.instance_eval do
          # DNSResolver only supports IPv4 so we can safely assume IPv4 address
          begin
            socket = TCPConnectSocket.new(::Socket::AF_INET, addr, port, host)
          rescue Errno::ENETUNREACH
            on_connect_failed
            return
          end

          initialize(socket, *args)
          @_connector = Socket::Connector.new(self, socket)
          @_resolver = nil
        end
        @sock.attach(evloop)
      end

      def on_failure
        @sock.__send__(:on_resolve_failed)
        @sock.instance_eval do
          @_resolver = nil
          @_failed = true
        end
        return
      end
    end
  end

  class UNIXSocket < Socket
    attr_reader :path, :address_family

    # Connect to the given UNIX domain socket
    def self.connect(path, *args)
      new(::UNIXSocket.new(path), *args)
    end

    def initialize(socket)
      raise ArgumentError, "socket must be a UNIXSocket" unless socket.is_a? ::UNIXSocket

      super
      @address_family, @path = socket.peeraddr
    end
  end
end
