module IO::Stream::Readable

def block_size

@returns [Integer] The minimum read size.
Legacy accessor for backwards compatibility
def block_size
	@minimum_read_size
end

def block_size=(value)

@parameter value [Integer] The minimum read size.
Legacy setter for backwards compatibility
def block_size=(value)
	@minimum_read_size = value
end

def close_read

Close the read end of the stream.
def close_read
end

def consume_read_buffer(size = nil, buffer = nil)

@returns [String | Nil] The consumed data, or nil if no data available.
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The amount of data to consume. If nil, consume entire buffer.
Consumes at most `size` bytes from the buffer.
def consume_read_buffer(size = nil, buffer = nil)
	# If we are at finished, and the read buffer is empty, we can't consume anything.
	if @finished && @read_buffer.empty?
		# Clear the buffer even when returning nil
		if buffer
			buffer.clear
			buffer.force_encoding(Encoding::BINARY)
		end
		return nil
	end
	
	result = nil
	
	if size.nil? or size >= @read_buffer.bytesize
		# Consume the entire read buffer:
		if buffer
			buffer.clear
			buffer << @read_buffer
			result = buffer
		else
			result = @read_buffer
		end
		@read_buffer = StringBuffer.new
	else
		# We know that we are not going to reuse the original buffer.
		# But byteslice will generate a hidden copy. So let's freeze it first:
		@read_buffer.freeze
		
		if buffer
			# Use replace instead of clear + << for better performance
			buffer.replace(@read_buffer.byteslice(0, size))
			result = buffer
		else
			result = @read_buffer.byteslice(0, size)
		end
		@read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
	end
	
	return result
end

def discard_until(pattern, offset = 0, limit: nil)

@returns [String | Nil] The contents of the stream up until the pattern, or nil if the pattern was not found.
@parameter limit [Integer] The maximum number of bytes to read, including the pattern.
@parameter offset [Integer] The offset to start searching from.
@parameter pattern [String] The pattern to match.
Efficiently discard data from the stream until encountering pattern.
def discard_until(pattern, offset = 0, limit: nil)
	if index = index_of(pattern, offset, limit, true)
		@read_buffer.freeze
		
		if limit and index >= limit
			@read_buffer = @read_buffer.byteslice(limit, @read_buffer.bytesize)
			
			return nil
		end
		
		matched = @read_buffer.byteslice(0, index+pattern.bytesize)
		@read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
		
		return matched
	end
end

def fill_read_buffer(size = @minimum_read_size)

Fills the buffer from the underlying stream.
def fill_read_buffer(size = @minimum_read_size)
	# Limit the read size to avoid exceeding SSIZE_MAX and to manage memory usage.
	# Very large reads can also hurt interactive performance by blocking for too long.
	if size > @maximum_read_size
		size = @maximum_read_size
	end
	
	# This effectively ties the input and output stream together.
	flush
	
	if @read_buffer.empty?
		if sysread(size, @read_buffer)
			# Console.info(self, name: "read") {@read_buffer.inspect}
			return true
		end
	else
		if chunk = sysread(size, @input_buffer)
			@read_buffer << chunk
			# Console.info(self, name: "read") {@read_buffer.inspect}
			
			return true
		end
	end
	
	# else for both cases above:
	@finished = true
	return false
end

def finish!

Mark the stream as finished and raise `EOFError`.
def finish!
	@read_buffer.clear
	@finished = true
	
	raise EOFError
end

def finished?

@returns [Boolean] If the stream is at file which means there is no more data to be read.

See {readable?} for a non-blocking alternative.
Determins if the stream has consumed all available data. May block if the stream is not readable.
def finished?
	if !@read_buffer.empty?
		return false
	elsif @finished
		return true
	else
		return !self.fill_read_buffer
	end
end

def gets(separator = $/, limit = nil, chomp: false)

@returns [String | Nil] The line read from the stream, or nil if at end of stream.
@parameter chomp [Boolean] Whether to remove the separator from the returned line.
@parameter limit [Integer | Nil] The maximum number of bytes to read.
@parameter separator [String] The line separator to search for.
Read a line from the stream, similar to IO#gets.
def gets(separator = $/, limit = nil, chomp: false)
	# Compatibility with IO#gets:
	if separator.is_a?(Integer)
		limit = separator
		separator = $/
	end
	
	# We don't want to split in the middle of the separator, so we subtract the size of the separator from the start of the search:
	split_offset = separator.bytesize - 1
	
	offset = 0
	
	until index = @read_buffer.index(separator, offset)
		offset = @read_buffer.bytesize - split_offset
		offset = 0 if offset < 0
		
		# If a limit was given, and the offset is beyond the limit, we should return up to the limit:
		if limit and offset >= limit
			# As we didn't find the separator, there is nothing to chomp either.
			return consume_read_buffer(limit)
		end
		
		# If we can't read any more data, we should return what we have:
		return consume_read_buffer unless fill_read_buffer
	end
	
	# If the index of the separator was beyond the limit:
	if limit and index >= limit
		# Return up to the limit:
		return consume_read_buffer(limit)
	end
	
	# Freeze the read buffer, as this enables us to use byteslice without generating a hidden copy:
	@read_buffer.freeze
	
	line = @read_buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize))
	@read_buffer = @read_buffer.byteslice(index+separator.bytesize, @read_buffer.bytesize)
	
	return line
end

def index_of(pattern, offset, limit, discard = false)

@returns [Integer | Nil] The index of the pattern, or nil if not found.
@parameter limit [Integer | Nil] The maximum number of bytes to read while searching.
@parameter offset [Integer] The offset to start searching from.
@parameter pattern [String] The pattern to search for.
Find the index of a pattern in the read buffer, reading more data if needed.
def index_of(pattern, offset, limit, discard = false)
n't want to split on the pattern, so we subtract the size of the pattern.
ffset = pattern.bytesize - 1
ndex = @read_buffer.index(pattern, offset)
 = @read_buffer.bytesize - split_offset
 = 0 if offset < 0
it and offset >= limit
n nil
 fill_read_buffer
n nil
card
we are discarding, we should consume the read buffer up to the offset:
me_read_buffer(offset)
t = 0
index

def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)

@parameter block_size [Integer] Legacy parameter, use minimum_read_size instead.
@parameter maximum_read_size [Integer] The maximum size for read operations.
@parameter minimum_read_size [Integer] The minimum size for read operations.
Initialize readable stream functionality.
def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)
	@finished = false
	@read_buffer = StringBuffer.new
	# Used as destination buffer for underlying reads.
	@input_buffer = StringBuffer.new
	
	# Support legacy block_size parameter for backwards compatibility
	@minimum_read_size = block_size || minimum_read_size
	@maximum_read_size = maximum_read_size
	
	super(**, &block) if defined?(super)
end

def peek(size = nil)

@returns [String] The data in the buffer without consuming it.
@parameter size [Integer | Nil] The number of bytes to peek at. If nil, peek at all available data.
Peek at data in the buffer without consuming it.
def peek(size = nil)
	if size
		until @finished or @read_buffer.bytesize >= size
			# Compute the amount of data we need to read from the underlying stream:
			read_size = size - @read_buffer.bytesize
			
			# Don't read less than @minimum_read_size to avoid lots of small reads:
			fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size)
		end
		return @read_buffer[..([size, @read_buffer.size].min - 1)]
	end
	until (block_given? && yield(@read_buffer)) or @finished
		fill_read_buffer
	end
	return @read_buffer
end

def read(size = nil, buffer = nil)

@returns [String] The data read from the stream, or the provided buffer filled with data.
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The number of bytes to read. If nil, read until end of stream.
Read data from the stream.
def read(size = nil, buffer = nil)
	if size == 0
		if buffer
			buffer.clear
			buffer.force_encoding(Encoding::BINARY)
			return buffer
		else
			return String.new(encoding: Encoding::BINARY)
		end
	end
	
	if size
		until @finished or @read_buffer.bytesize >= size
			# Compute the amount of data we need to read from the underlying stream:
			read_size = size - @read_buffer.bytesize
			
			# Don't read less than @minimum_read_size to avoid lots of small reads:
			fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size)
		end
	else
		until @finished
			fill_read_buffer
		end
		
		if buffer
			buffer.replace(@read_buffer)
			@read_buffer.clear
		else
			buffer = @read_buffer
			@read_buffer = StringBuffer.new
		end
		
		# Read without size always returns a non-nil value, even if it is an empty string.
		return buffer
	end
	
	return consume_read_buffer(size, buffer)
end

def read_exactly(size, buffer = nil, exception: EOFError)

@returns [String] The data read from the stream.
@parameter exception [Class] The exception to raise if not enough data is available.
@parameter size [Integer] The number of bytes to read.
Read exactly the specified number of bytes.
def read_exactly(size, buffer = nil, exception: EOFError)
	if buffer = read(size, buffer)
		if buffer.bytesize != size
			raise exception, "Could not read enough data!"
		end
		
		return buffer
	end
	
	raise exception, "Stream finished before reading enough data!"
end

def read_partial(size = nil, buffer = nil)

@returns [String] The data read from the stream, or the provided buffer filled with data.
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The number of bytes to read. If nil, read all available data.
Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
def read_partial(size = nil, buffer = nil)
	if size == 0
		if buffer
			buffer.clear
			buffer.force_encoding(Encoding::BINARY)
			return buffer
		else
			return String.new(encoding: Encoding::BINARY)
		end
	end
	
	if !@finished and @read_buffer.empty?
		fill_read_buffer
	end
	
	return consume_read_buffer(size, buffer)
end

def read_until(pattern, offset = 0, limit: nil, chomp: true)

@returns [String | Nil] The contents of the stream up until the pattern, or nil if the pattern was not found.
@parameter chomp [Boolean] Whether to remove the pattern from the returned data.
@parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
@parameter offset [Integer] The offset to start searching from.
@parameter pattern [String] The pattern to match.
Efficiently read data from the stream until encountering pattern.
def read_until(pattern, offset = 0, limit: nil, chomp: true)
	if index = index_of(pattern, offset, limit)
		return nil if limit and index >= limit
		
		@read_buffer.freeze
		matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
		@read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
		
		return matched
	end
end

def readable?

@returns [Boolean] If the stream is readable, i.e. a `read` operation has a chance of success.
Whether there is a chance that a read operation will succeed or not.
def readable?
	# If we are at the end of the file, we can't read any more data:
	if @finished
		return false
	end
	
	# If the read buffer is not empty, we can read more data:
	if !@read_buffer.empty?
		return true
	end
	
	# If the underlying stream is readable, we can read more data:
	return !closed?
end

def readpartial(size = nil, buffer = nil)

@returns [String] The data read from the stream.
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The number of bytes to read.
This is a compatibility shim for existing code that uses `readpartial`.
def readpartial(size = nil, buffer = nil)
	read_partial(size, buffer) or raise EOFError, "Stream finished before reading enough data!"
end