# This code is free software; you can redistribute it and/or modify it under
# the terms of the new BSD License.
#
# Copyright (c) 2011-2012, Sebastian Staudt

require 'socket'

module SteamCondenser

  module Servers

    # This module is included by all classes implementing server functionality
    #
    # It provides basic name resolution features and the ability to rotate between
    # different IP addresses belonging to a single DNS name.
    #
    # @author Sebastian Staudt
    module BaseServer

      # Returns a list of host names associated with this server
      #
      # @return [Array<String>] The host names of this server
      attr_reader :host_names

      # Returns a list of IP addresses associated with this server
      #
      # @return [Array<String>] The IP addresses of this server
      attr_reader :ip_addresses

      # Creates a new server instance with the given address and port
      #
      # @param [String] address Either an IP address, a DNS name or one of them
      #        combined with the port number. If a port number is given, e.g.
      #        'server.example.com:27016' it will override the second argument.
      # @param [Fixnum] port The port the server is listening on
      # @see init_socket
      # @raise [Error] if an host name cannot be resolved
      def initialize(address, port = nil)
        address = address.to_s
        address, port = address.split(':', 2) if address.include? ':'

        @host_names   = []
        @ip_addresses = []
        @ip_index     = 0
        @port         = port

        Socket.getaddrinfo(address, port, Socket::AF_INET, Socket::SOCK_DGRAM).
               each do |address|
          @host_names << address[2]
          @ip_addresses << address[3]
        end

        @ip_address = @ip_addresses.first

        init_socket
      end

      # Disconnect the connections to this server
      #
      # @note In the base implementation this does nothing, only connection-based
      #       communication channels have to be disconnected.
      def disconnect
      end

      # Rotate this server's IP address to the next one in the IP list
      #
      # If this method returns `true`, it indicates that all IP addresses have been
      # used, hinting at the server(s) being unreachable. An appropriate action
      # should be taken to inform the user.
      #
      # Servers with only one IP address will always cause this method to return
      # `true` and the sockets will not be reinitialized.
      #
      # @return [Boolean] `true`, if the IP list reached its end. If the list
      #         contains only one IP address, this method will instantly return
      #         `true`
      # @see #init_socket
      def rotate_ip
        return true if @ip_addresses.size == 1

        @ip_index = (@ip_index + 1) % @ip_addresses.size
        @ip_address = @ip_addresses[@ip_index]

        init_socket

        @ip_index == 0
      end

      protected

      # Execute an action in the context of this server's current IP address
      #
      # Any failure will cause the IP to rotate to the next IP in the server's IP
      # list and the execution will be repeated for the next IP address. If the IP
      # rotation reaches the end of the list, the error will be reraised.
      #
      # @param [Proc] proc The action to be executed in a failsafe way
      # @see #rotate_ip
      def failsafe(&proc)
        begin
          proc.call
        rescue
          raise $! if rotate_ip
          log.info "Request failed, retrying for #@ip_address..."
          failsafe &proc
        end
      end

      # Initializes the socket(s) to communicate with the server
      #
      # @abstract Must be implemented in including classes to prepare sockets for
      #           server communication
      def init_socket
      end

    end
  end
end
