module LinuxStat
	# Shows various OS related information of the current system.

	module OS
		class << self
			##
			# Reads /etc/os-release and returns a Hash. For example:
			#    LinuxStat::OS.os_release
			#
			#    => {:NAME=>"Arch Linux", :PRETTY_NAME=>"Arch Linux", :ID=>"arch", :BUILD_ID=>"rolling",
			# :ANSI_COLOR=>"38;2;23;147;209", :HOME_URL=>"https://www.archlinux.org/",
			# :DOCUMENTATION_URL=>"https://wiki.archlinux.org/",
			# :SUPPORT_URL=>"https://bbs.archlinux.org/", :BUG_REPORT_URL=>"https://bugs.archlinux.org/",
			# :LOGO=>"archlinux"}
			#
			# If the info isn't available, it will return an empty Hash.
			#
			# The amount of data read is 4096 bytes. Any more than that will result in truncated output.
			#
			# The information is also cached, and once loaded, won't change in runtime.
			# Because changing the /etc/lsb-release
			# isn't expected in runtime.
			def os_release
				@@os_release ||= File.readable?('/etc/os-release') ? release('/etc/os-release') : {}
			end

			##
			# Reads /etc/lsb-release and returns a Hash. For example:
			#    LinuxStat::OS.lsb_release
			#
			#    => {:LSB_VERSION=>"1.4", :DISTRIB_ID=>"Arch", :DISTRIB_RELEASE=>"rolling", :DISTRIB_DESCRIPTION=>"Arch Linux"}
			#
			# If the info isn't available, it will return an empty Hash.
			#
			# The amount of data read is 4096 bytes. Any more than that will result in truncated output.
			#
			# The information is also cached, and once loaded, won't change in runtime.
			# Because changing the /etc/lsb-release isn't expected in runtime.
			def lsb_release
				@@lsb_release ||= File.readable?('/etc/lsb-release') ? release('/etc/lsb-release') : {}
			end

			##
			# Reads /etc/lsb-release or /etc/os-release tries to get information about the distribution.
			#
			# If the information isn't available, it will read and return /etc/issue.
			#
			# The return type is String.
			# If none of the info is available, it will return an empty frozen String.
			def distribution
				@@distribution ||= if os_release.key?(:NAME)
					os_release[:NAME]
				else
					v = lsb_release

					if v.key?(:DISTRIB_DESCRIPTION)
						v[:DISTRIB_DESCRIPTION]
					elsif v.key?(:DISTRIB_ID)
						v[:DISTRIB_ID]
					elsif File.readable?('/etc/issue'.freeze)
						IO.read('/etc/issue'.freeze, encoding: 'ASCII-8bit'.freeze).strip
					else
						'Unknown'.freeze
					end
				end
			end

			##
			# Gives you the version of the OS you are using.
			#
			# On ArchLinux for example:
			#    LinuxStat::OS.version
			#
			#    => "rolling"
			#
			# On Ubuntu 20.04:
			#    LinuxStat::OS.version
			#
			#    => "20.04"
			#
			# On CentOS 26:
			#    LinuxStat::OS.version
			#
			#    => "26"
			#
			# The return type is String.
			# But if the information isn't available, it will return an empty frozen String.
			def version
				@@distrib_version ||= if os_release.key?(:VERSION_ID)
					os_release[:VERSION_ID]
				elsif lsb_release.key?(:DISTRIB_RELEASE)
					lsb_release[:DISTRIB_RELEASE]
				elsif os_release.key?(:VERSION)
					os_release[:VERSION]
				else
					''.freeze
				end
			end

			##
			# Uses utsname.h to determine the machine
			#
			# It returns a String but if the info isn't available, it will return an empty String
			def machine
				@@machine ||= LinuxStat::Uname.machine
			end

			##
			# Uses utsname.h to determine the system nodename
			#
			# It returns String but if the info isn't available, it will return an empty String.
			def nodename
				LinuxStat::Uname.nodename
			end

			##
			# Returns the hostname from LinuxStat::Sysconf.hostname.
			#
			# The return type is String.
			# If the info info isn't available, it will return an empty String.
			def hostname
				LinuxStat::Sysconf.hostname
			end

			##
			# Reads ruby configuration and tries to guess if the system is 64 bit.
			#
			# If it fails then it runs utsname.h to guess the machine.
			#
			# It the machine is 64 bits, it will return 64, else it returns 32.
			#
			# The return type is strictly Integer and doesn't fail.
			def bits
				@@bits ||= if machine.end_with?('64') || RbConfig::CONFIG['host_cpu'].end_with?('64') || RUBY_PLATFORM.end_with?('64')
					64
				else
					32
				end
			end

			##
			# Reads /proc/uptime and returns the system uptime:
			#    LinuxStat::OS.uptime
			#
			#    => {:hour=>16, :minute=>10, :second=>11, :jiffy=>20}
			#
			# Using uptime is 10x slower than using uptime_i
			#
			# If the stat isn't available, an empty hash is returned.
			def uptime
				_uptime = LinuxStat::ProcFS.uptime_f
				return {} unless _uptime

				uptime_i = _uptime.to_i

				h = uptime_i / 3600
				m = uptime_i % 3600 / 60
				s = uptime_i.%(60)
				j = _uptime.-(uptime_i) * 100

				{
					hour: h,
					minute: m,
					second: s,
					jiffy: j.to_i
				}
			end

			##
			# Returns Float uptime of the system reported by /proc/uptime
			#    LinuxStat::OS.uptime_f
			#
			#    => 28956.34
			#
			# The value is generally rounded to 2 decimal places.
			#
			# Using uptime_f is 10x slower than using uptime_i
			#
			# If the stat isn't available, nil is returned.
			def uptime_f
				LinuxStat::ProcFS.uptime_f
			end

			##
			# Returns uptime of the system reported by LinuxStat::Sysinfo.uptime()
			#    LinuxStat::OS.uptime_i
			#
			#    => 28956
			#
			# If the stat isn't available, nil is returned.
			def uptime_i
				LinuxStat::Sysinfo.uptime
			end

			##
			# The first three fields in this file are load average
              		# figures giving the number of jobs in the run queue (state R)
			# or waiting for disk I/O (state D) averaged over 1, 5,
			# and 15 minutes.  They are the same as the load average
			# numbers given by uptime(1) and other programs.
			# https://man7.org/linux/man-pages/man5/procfs.5.html
			#
			# The return type is an Hash containing the values
			# that maps to 1, 5, and 15.
			# This method calls LinuxStat::Sysinfo.loads() directly.
			#
			# However, if the info isn't available, it will return nil as values.
			def loadavg
				loads = LinuxStat::Sysinfo.loads

				{
					1 => loads[0],
					5 => loads[1],
					15 => loads[2]
				}
			end

			alias distrib_version version

			private
			def release(file)
				IO.readlines(file, 4096).reduce({}) { |h, x|
					x.strip!
					next h if x.empty?

					splitted = x.split(?=.freeze)

					key = splitted[0].to_s
					key.strip!

					value = splitted[1..-1].join(?=.freeze).to_s
					value.strip!

					value[0] = ''.freeze if value[0] == ?".freeze
					value[-1] = ''.freeze if value[-1] == ?".freeze

					h.store(key.to_sym, value)
					h
				}
			end
		end
	end
end
