require 'rethtool'
require 'rethtool/ethtool_cmd'

# All of the settings of a network interface.  Ridiculous amounts of
# info is available; we only support a subset of them at present.
#
# Create an instance of this class with the interface name as the only
# parameter, then use the available instance methods to get the info you
# seek:
#
#    if = Rethtool::InterfaceSettings.new("eth0")
#    puts "Current link mode is #{if.current_mode}"
#
class Rethtool::InterfaceSettings
	Mode = Struct.new(:speed, :duplex, :media)

	# A struct to represent interface modes (supported, advertised, current)
	#
	# available fields are:
	#
	#  .speed -- integer link speed, in Mb (-1 if unknown)
	#  .duplex -- :full, :half, :fec, or :unknown
	#  .media -- A string, such as 'T', 'X', 'KX', etc, or nil if unknown
	#
	class Mode
		# Print out a more standard-looking representation for a mode
		def to_s
			if self.speed == :unknown
				"Unknown"
			else
				"#{self.speed}base#{self.media}/#{self.duplex}"
			end
		end
	end
	
	# Create a new InterfaceSettings object.  Simply pass it the name of the
	# interface you want to get the settings for.
	def initialize(interface)
		@interface = interface
		cmd = Rethtool::EthtoolCmd.new
		cmd.cmd = Rethtool::ETHTOOL_CMD_GSET
		
		@data = Rethtool.ioctl(interface, cmd)
	end
	
	# Return an array of the modes supported by the interface.  Returns an
	# array of Mode objects.
	def supported_modes
		modes(@data.supported)
	end
	
	# Return an array of the modes advertised by the interface.  Returns an
	# array of Mode objects.  If you know the difference between 'supported'
	# and 'advertised', you're one up on me.
	def advertised_modes
		modes(@data.advertising)
	end
	
	# Return a Mode object representing the current detected mode of the
	# interface.
	def current_mode
		speed = @data.speed
		speed = :unknown if speed == 65535
		
		duplex = case @data.duplex
			when 0 then :half
			when 1 then :full
			else        :unknown
		end
		
		port = case @data.port
			when 0   then 'T'
			when 1   then 'AUI'
			when 2   then 'MII'
			when 3   then 'F'
			when 4   then 'BNC'
			when 255 then 'Other'
			else          'Unknown'
		end
		
		Mode.new(speed, duplex, port)
	end
	
	# Return the "best" possible mode supported by this interface.
	# This is the highest speed mode with the "best" duplex
	# (fec > full > half).
	def best_mode
		modes = self.advertised_modes
		best_speed = modes.map { |m| m.speed }.sort.last
		high_speed_modes = modes.find_all { |m| m.speed == best_speed }

		# Somewhere recently, RHEL decided to release a kernel or libc update
		# that changes the behaviour of the ethtool ioctl so that instead of
		# returning EOPNOTSUPP when you ask for available speeds on an interface
		# that doesn't support that (like bonded NICs), it now returns success with
		# an empty list.  WHO DOES THAT SORT OF SHIT?!?  So we've got to fake it
		# ourselves.
		raise Errno::EOPNOTSUPP.new("#{@interface} doesn't support enumerating speed modes") if modes.empty?

		if high_speed_modes.length == 0
			raise RuntimeError.new("Can't happen: no modes with the best speed?!?")
		elsif high_speed_modes.length == 1
			high_speed_modes.first
		else
			duplexes = high_speed_modes.map { |m| m.duplex }
			best_duplex = if duplexes.include? :fec
				:fec
			elsif duplexes.include? :full
				:full
			else
				:half
			end
			high_speed_modes.find { |m| m.duplex == best_duplex }
		end
	end
	
	private
	
	PossibleModes = {
			  1 << 0  => Mode.new(10, :half, 'T'),
			  1 << 1  => Mode.new(10, :full, 'T'),
			  1 << 2  => Mode.new(100, :half, 'T'),
			  1 << 3  => Mode.new(100, :full, 'T'),
			  1 << 4  => Mode.new(1000, :half, 'T'),
			  1 << 5  => Mode.new(1000, :full, 'T'),
			  1 << 12 => Mode.new(10000, :full, 'T'),
			  1 << 15 => Mode.new(2500, :full, 'X'),
			  1 << 17 => Mode.new(1000, :full, 'KX'),
			  1 << 18 => Mode.new(10000, :full, 'KX4'),
			  1 << 19 => Mode.new(10000, :full, 'KR'),
			  1 << 20 => Mode.new(10000, :fec, 'R')
	}

	# Turn a uint32 of bits into a list of supported modes.  Sigh.
	def modes(data)
		PossibleModes.find_all { |m| (m[0] & data) > 0 }.map { |m| m[1] }
	end
end
