# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2019-2026, by Samuel Williams.

require_relative "memory"
require "set"
require "json"

module Process
	module Metrics
		DURATION = /\A
			(?:(?<days>\d+)-)?              # Optional days (e.g., '2-')
			(?:(?<hours>\d+):)?             # Optional hours (e.g., '1:')
			(?<minutes>\d{1,2}):            # Minutes (always present, 1 or 2 digits)
			(?<seconds>\d{2})               # Seconds (exactly 2 digits)
			(?:\.(?<fraction>\d{1,2}))?     # Optional fraction of a second (e.g., '.27')
		\z/x
		
		
		# Parse a duration string into seconds.
		# According to the linux manual page specifications.
		def self.duration(value)
			if match = DURATION.match(value)
				days = match[:days].to_i
				hours = match[:hours].to_i
				minutes = match[:minutes].to_i
				seconds = match[:seconds].to_i
				fraction = match[:fraction].to_i
				
				return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds + fraction / 100.0
			else
				return 0.0
			end
		end
		
		# General process information.
		class General < Struct.new(:process_id, :parent_process_id, :process_group_id, :processor_utilization, :virtual_size, :resident_size, :processor_time, :elapsed_time, :start_time, :command, :memory)
			# Convert the object to a JSON serializable hash.
			def as_json
				{
					process_id: self.process_id,
					parent_process_id: self.parent_process_id,
					process_group_id: self.process_group_id,
					processor_utilization: self.processor_utilization,
					total_size: self.total_size,
					virtual_size: self.virtual_size,
					resident_size: self.resident_size,
					processor_time: self.processor_time,
					elapsed_time: self.elapsed_time,
					start_time: self.start_time,
					command: self.command,
					memory: self.memory&.as_json,
				}
			end
			
			# Convert the object to a JSON string.
			def to_json(*arguments)
				as_json.to_json(*arguments)
			end
			
			# The total size of the process in memory, in bytes.
			def total_size
				if memory = self.memory
					memory.proportional_size
				else
					self.resident_size
				end
			end
			
			alias memory_usage total_size
			
			# Recursively expand a set of child PIDs into a collection.
			# @parameter children [Array<Integer>] The list of child process IDs to expand.
			# @parameter hierarchy [Hash<Integer, Array<Integer>>] The parent-to-children process hierarchy.
			# @parameter pids [Set<Integer>] The set to populate with process IDs.
			def self.expand_children(children, hierarchy, pids)
				children.each do |pid|
					self.expand(pid, hierarchy, pids)
				end
			end
			
			# Recursively expand a process and its descendants into a collection.
			# @parameter pid [Integer] The process ID to expand.
			# @parameter hierarchy [Hash<Integer, Array<Integer>>] The parent-to-children process hierarchy.
			# @parameter pids [Set<Integer>] The set to populate with process IDs.
			def self.expand(pid, hierarchy, pids)
				unless pids.include?(pid)
					pids << pid
					
					if children = hierarchy.fetch(pid, nil)
						self.expand_children(children, hierarchy, pids)
					end
				end
			end
			
			# Build a parent-to-children process hierarchy from a set of processes.
			# @parameter processes [Hash<Integer, General>] A hash mapping PIDs to General instances.
			# @returns [Hash<Integer, Array<Integer>>] A hash mapping each parent PID to an array of child PIDs.
			def self.build_tree(processes)
				hierarchy = Hash.new{|h,k| h[k] = []}
				
				processes.each_value do |process|
					if parent_process_id = process.parent_process_id
						hierarchy[parent_process_id] << process.process_id
					end
				end
				
				return hierarchy
			end
			
			# Capture detailed memory metrics for each process in the given collection.
			# @parameter processes [Hash<Integer, General>] A hash mapping PIDs to General instances.
			def self.capture_memory(processes)
				count = processes.size
				
				processes.each do |pid, process|
					process.memory = Memory.capture(pid, count: count)
				end
			end
		end
	end
end

# One backend provides General.capture: Linux uses /proc (no subprocess); other platforms use ps.
if RUBY_PLATFORM.include?("linux")
	require_relative "general/linux"
else
	require_relative "general/process_status"
end
