# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025-2026, by Samuel Williams.
# Copyright, 2026, by Fletcher Dares.

require "io/event/timers"

module IO::Event
	# A test fiber scheduler that uses WorkerPool for blocking operations.
	# 
	# This scheduler implements the fiber scheduler interface and delegates
	# blocking operations to a WorkerPool instance for testing.
	#
	# @example Testing usage
	# ```ruby
	# # Create with default selector and worker pool
	# scheduler = IO::Event::TestScheduler.new
	# 
	# # Or provide custom selector and/or worker pool
	# selector = IO::Event::Selector.new(Fiber.current)
	# worker_pool = IO::Event::WorkerPool.new(maximum_worker_count: 4)
	# scheduler = IO::Event::TestScheduler.new(selector: selector, worker_pool: worker_pool)
	# 
	# Fiber.set_scheduler(scheduler)
	# 
	# # Standard Ruby operations that use rb_nogvl will be handled by the worker pool
	# # Examples: sleep, file I/O, network operations, etc.
	# Fiber.schedule do
	#   sleep(0.001)  # This triggers rb_nogvl and uses the worker pool
	# end.resume
	# ```
	class TestScheduler
		# Optional fiber scheduler hooks that we forward to the underlying selector. Mixed into the scheduler's singleton class only when the selector actually implements the corresponding method, so feature detection via `respond_to?` reflects the real backend (and Ruby falls back to its default behaviour otherwise).
		module Forwarders
			# Fiber scheduler hook for `IO#close`. Ruby invokes this with a raw integer file descriptor (Ruby 4.0+).
			def io_close(descriptor)
				@selector.io_close(descriptor)
			end
		end
		
		def initialize(selector: nil, worker_pool: nil, maximum_worker_count: nil)
			@selector = selector || ::IO::Event::Selector.new(Fiber.current)
			
			if ::IO::Event.const_defined?(:WorkerPool)
				@worker_pool = worker_pool || ::IO::Event::WorkerPool.new(maximum_worker_count: maximum_worker_count)
			end
			
			@timers = ::IO::Event::Timers.new
			
			# Track the number of fibers that are blocked.
			@blocked = 0
			@blocking = {}
			
			install_optional_forwarders(@selector)
		end
		
		private def install_optional_forwarders(selector)
			forwarders = nil
			
			Forwarders.instance_methods(false).each do |name|
				next unless selector.respond_to?(name)
				forwarders ||= Module.new
				forwarders.define_method(name, Forwarders.instance_method(name))
			end
			
			singleton_class.include(forwarders) if forwarders
		end
		
		# @attribute [WorkerPool] The worker pool used for executing blocking operations.
		attr_reader :worker_pool
		
		# @attribute [IO::Event::Selector] The I/O event selector used for managing fiber scheduling.
		attr_reader :selector
		
		if ::IO::Event.const_defined?(:WorkerPool)
			# Optional fiber scheduler hook - delegates to WorkerPool:
			def blocking_operation_wait(operation)
				@blocked += 1
				# Submit the operation to the worker pool and wait for completion
				@worker_pool&.call(operation)
			ensure
				@blocked -= 1
			end
		end
		
		# Required fiber scheduler hooks
		def close
			@selector&.close
			@worker_pool&.close
			@worker_pool = nil
		end
		
		def block(blocker, timeout = nil)
			fiber = Fiber.current
			
			if timeout
				timer = @timers.after(timeout) do
					if fiber.alive?
						fiber.transfer(false)
					end
				end
			end
			
			begin
				@blocked += 1
				@blocking[fiber] = true
				@selector.transfer
			ensure
				@blocking.delete(fiber)
				@blocked -= 1
			end
		ensure
			timer&.cancel!
		end
		
		def unblock(blocker, fiber)
			if selector = @selector
				selector.push(fiber)
				selector.wakeup
			end
		end
		
		class FiberInterrupt
			def initialize(fiber, exception)
				@fiber = fiber
				@exception = exception
			end
			
			def alive?
				@fiber.alive?
			end
			
			def transfer
				@fiber.raise(@exception)
			end
		end
		
		def fiber_interrupt(fiber, exception)
			unblock(nil, FiberInterrupt.new(fiber, exception))
		end
		
		def io_wait(io, events, timeout = nil)
			fiber = Fiber.current
			
			if timeout
				timer = @timers.after(timeout) do
					fiber.transfer
				end
			end
			
			begin
				@blocked += 1
				return @selector.io_wait(fiber, io, events)
			ensure
				@blocked -= 1
			end
		ensure
			timer&.cancel!
		end
		
		def process_wait(pid, flags)
			@blocked += 1
			@selector.process_wait(Fiber.current, pid, flags)
		ensure
			@blocked -= 1
		end
		
		def kernel_sleep(duration = nil)
			if duration
				self.block(nil, duration)
			else
				@selector.transfer
			end
		end
		
		def fiber(&block)
			Fiber.new(&block).tap(&:transfer)
		end
		
		# Run the scheduler event loop
		def run
			while @blocked > 0 or @timers.size > 0
				interval = @timers.wait_interval
				@selector.select(interval)
				@timers.fire
			end
		end
		
		def scheduler_close(error = $!)
			self.run
		ensure
			self.close
		end
		
		private
		
		def transfer
			@selector.transfer
		end
		
		def push(fiber)
			@selector.push(fiber)
		end
		
		def wakeup
			@selector.wakeup
		end
	end
end 
