english
version "1.0"
identify "xyz"

#: Copyright (c) 1998-2007 by Wayne C. Gramlich.
#, All rights reserved.

#: This module provides an input stream interface to files, network
#, streams, pipes, strings, and filters.
#,
#, Eventually, this module needs to be implementated via a co-type
#, (polymorphic) interface; until then, a simple variant is used.

module input_stream

import
    address
    character
    file_name
    file_set
    format
    in_stream
    integer
    logical
    memory
    null
    out_stream
    string
    system
    unix_system
    unsigned


define input_stream			#: Input stream of bytes/characters
    variant kind input_stream_kind
	closed null			#: This input_stream is closed
	file_input_stream file_input_stream #: A standard file input stream
    generate address_get, allocate, erase, identical, print

define input_stream_kind		#: The {input_stream} types.
    enumeration
	closed				#: A closed input stream
	file_input_stream		#: A file input stream
    generate equal, print, unsigned_convert

define file_input_stream		#: Input stream from a file
    record
	memory memory			#: Memory buffer
	done logical			#: {true} => no more data is available
	file_set file_set		#: {file_set} polling object (or ??)
	file_descriptor_number unsigned	#: File descriptor number
	file_name file_name		#: File name
	file_name_string string		#: Associated file name as a {string}
	offset unsigned			#: Offset to next valid byte
	remaining unsigned		#: Remaining valid bytes
    generate allocate, erase, print


#: {input_stream} routines:

procedure character_read@input_stream
    takes
	input_stream input_stream
    returns character

    #: This procedure will return the next character from {input_stream}.
    #, If there are no more characters to be had, the end-of-stream
    #, character is returned (see {end_of_stream_character}@{input_stream}.

    character:: character := ??
    extract input_stream
      tag closed:: null := closed
	assert false
      tag file_input_stream:: file_input_stream := file_input_stream
	character :@= character_read@(file_input_stream)


procedure close@input_stream
    takes
	input_stream input_stream
    returns_nothing

    #: This procedure will close {input_stream}.

    extract input_stream
      tag closed:: null := closed
	assert false
      tag file_input_stream:: file_input_stream := file_input_stream
	close@(file_input_stream)
    input_stream.closed := ??


procedure create@input_stream
    takes_nothing
    returns input_stream

    #: This procedure will return a new closed {input_stream} object.
    #, It is primarily intended for use by other types, like
    #, {file_input_stream}, that have open procedures that will
    #, return an open {input_stream} object.

    input_stream :@= allocate@input_stream()
    input_stream.closed := ??
    return input_stream


procedure input_available@input_stream
    takes
	input_stream input_stream
    returns logical

    #: This procedure will return {true}@{logical} if there is at least
    #, one byte of input avaiable from {input_stream} and {false}@{logical}
    #, otherwise.  This procedure does not do read ahead, so it will not
    #, block.

    result:: logical := false
    extract input_stream
      tag closed:: null := closed
	result := false
      tag file_input_stream:: file_input_stream := file_input_stream
	result := input_available@(file_input_stream)
    return result


procedure end_of_stream_character@input_stream
    takes_nothing
    returns character

    #: This procedure returns the end-of-stream character that gets returned
    #, by {character_read}@{input_stream}() when there are no more characters
    #, to be had.

    return character_convert@(0xffffffff)	# I.e. -1


procedure is_done@input_stream
    takes
	input_stream input_stream
    returns logical

    #: This procedure will return {true}@{logical}() if no more
    #, bytes/characters can be read from {input_stream}; otherwise,
    #, {false} is returned.  Please note, this procedure will return
    #, {true} only until after at least one call is made to a read
    #, procedure returns 0 (or the end-of-stream character for
    #, {character_read}@{input_stream}().)

    extract input_stream
      tag closed:: null := closed
	return true
      tag file_input_stream:: file_input_stream := file_input_stream
	return is_done@(file_input_stream)


procedure is_open@input_stream
    takes
	input_stream input_stream
    returns logical

    #: This procedure will return {true}@{logical} if {input_stream} is
    #, still open and {false} otherwise.  Please note that having an
    #, open {input_stream} does not mean that any more data can be
    #, successfully read from it; use {is_done}@{input_stream}() for that
    #, purpose.

    return input_stream.kind != closed


procedure memory_read@input_stream
    takes
	input_stream input_stream
	memory memory
	offset unsigned
	amount_requested unsigned
    returns unsigned

    #: This procedure will read up to {amount_requested} bytes from
    #, {input_stream} and store them into {memory} starting at
    #, {offset}.  The actual number of bytes read in is returned.
    #, A return value of 0 means that there are no more bytes/characters
    #, available from {input_stream} and it can be closed via
    #, {close}@{input_stream}().
    #,
    #, Please note that this procedure will only block if any internal
    #, buffer associated with {input_stream} is empty and the additional
    #, data has not yet been delivered by the operating system.  Thus,
    #, you should expect the actual number of bytes returned will frequently
    #, be less than {amount_requested} even though even though the
    #, operating system may eventually be able deliver more than
    #, {amount_requested} with subsequent calls.  With this procedure
    #, you should never assume that return value of less than
    #, {amount_requested} means that end of stream has been reached.

    amount_read :@= 0
    extract input_stream
      tag closed:: null := closed
	assert false
      tag file_input_stream:: file_input_stream := file_input_stream
	amount_read :=
	  memory_read@(file_input_stream, memory, offset, amount_requested)
    return amount_read


procedure memory_read_exact@input_stream
    takes
	input_stream input_stream
	memory memory
	offset unsigned
	amount_requested unsigned
    returns unsigned

    #: This procedure will attempt to read exactly {amount_requested}
    #, bytes/characters from {input_stream} and store the result into
    #, {memory} at starting it {offset}.  The actual number of bytes
    #, read is returned.  The only time the return value will not
    #, be equal to {amount_requested} is when an end of file/stream
    #, condition has occured on {input_stream}, in which case, the
    #, remaining number of bytes/characters is returned.

    total_read :@= 0
    size :@= memory.size
    assert offset + amount_requested < size
    loop
	until amount_requested = 0
	amount_read :@=
	  memory_read@(input_stream, memory, offset, amount_requested)
	until amount_read = 0
	total_read :+= amount_read
	offset :+= amount_read
	amount_requested :-= amount_read
    return total_read	


procedure string_append@input_stream
    takes
	input_stream input_stream
	buffer string
	amount_requested unsigned
    returns unsigned

    #: This procedure will append up to {amount_requested} characters
    #, from {input_stream} to {buffer}.  The actual number of characters
    #, appended is returned.  A return value of 0 means that there are
    #, no more characters available from {input_stream} and it can be
    #, closed.
    #,
    #, Please note that this procedure will only block if any internal
    #, buffer associated with {input_stream} is empty and the additional
    #, data has not yet been delivered by the operating system.  Thus,
    #, you should expect the actual number of bytes returned will frequently
    #, be less than {amount_requested} even though even though the
    #, operating system may eventually be able deliver more than
    #, {amount_requested} with subsequent calls.  With this procedure
    #, you should never assume that return value of less than
    #, {amount_requested} means that end of stream has been reached.

    amount_read :@= 0
    extract input_stream
      tag closed:: null := closed
	assert false
      tag file_input_stream:: file_input_stream := file_input_stream
	amount_read :=
	  string_append@(file_input_stream, buffer, amount_requested)
    return amount_read


procedure string_append_exact@input_stream
    takes
	input_stream input_stream
	buffer string
	amount_requested unsigned
    returns unsigned

    #: This procedure will append exactly {amount_requested} bytes/characters
    #, from {input_stream} to {buffer}.  The total number of bytes/characters
    #, actually appended is returned.  The total number of bytes/characters
    #, returned will always equal {amount_requested} unless an end of
    #, stream condition occurs before {amount_requested} bytes/characters
    #, can be read.

    total_read :@= 0
    loop
	until amount_requested = 0
	amount_read :@=
	  string_append@(input_stream, buffer, amount_requested)
	until amount_read = 0
	total_read :+= amount_read
	amount_requested :-= amount_read
    return total_read	


#: {file_input_stream} routines:

procedure character_read@file_input_stream
    takes
	file_input_stream file_input_stream
    returns character

    #: This procedure will return the next character from {file_input_stream}.
    #, If there are no more characters to be had, the end-of-file character
    #, is returned (see {end_of_stream_character}@{file_input_stream}.

    memory :@= file_input_stream.memory
    memory_size :@= memory.size
    remaining :@= file_input_stream.remaining
    if remaining = 0
	remaining := fill@(file_input_stream, 1)
	if remaining = 0
	    return end_of_stream_character@input_stream()
    offset :@= file_input_stream.offset
    byte :@= memory[offset]
    file_input_stream.offset := offset + 1
    file_input_stream.remaining := remaining - 1
    return character_convert@(byte)    


procedure close@file_input_stream
    takes
	file_input_stream file_input_stream
    returns_nothing

    #: This procedure will close {file_input_stream}.

    unix_system:: unix_system := ??
    close@(unix_system, file_input_stream.file_descriptor_number)
    assert unix_system.status = ok

    memory :@= file_input_stream.memory
    resize@(memory, 0)		# Release most of the storage now!
    file_input_stream.offset := 0
    file_input_stream.remaining := 0


procedure file_descriptor_bind@file_input_stream
    takes
	file_descriptor_number unsigned
	buffer_size unsigned
	file_name file_name
	file_name_string string
    returns input_stream

    #: This procedure will create and return an {file_input_stream}
    #, object connected to {file_descriptor_number} with a buffer that
    #, contains {buffer_size} bytes.  {file_name} and {file_name_string}
    #, are associated with the returned {file_input_stream} object for
    #, subsequent print out.

    # Create and initialize the {file_input_stream} object:
    initialize file_input_stream::
      file_input_stream := allocate@file_input_stream()
	file_input_stream.memory := create@memory(buffer_size)
	file_input_stream.done := false	
	file_input_stream.file_descriptor_number := file_descriptor_number
	file_input_stream.file_name := file_name
	file_input_stream.file_name_string := file_name_string
	file_input_stream.offset := 0
	file_input_stream.remaining := 0

    # Create, initialize, and return the {input_stream} object:
    input_stream :@= create@input_stream()
    input_stream.file_input_stream := file_input_stream
    return input_stream


procedure fill@file_input_stream
    takes
	file_input_stream file_input_stream
	maximum_requested unsigned
    returns unsigned

    #: This procedure will fill the {file_input_stream} buffer with
    #, up to {maximum_requested} bytes/characters of data.  The amount
    #, of data remaining in the buffer is returned.

    assert file_input_stream.remaining = 0

    memory :@= file_input_stream.memory
    memory_size :@= memory.size
    assert memory_size != 0	# This fails if {file_input_stream} is closed
    assert file_input_stream.remaining = 0
    file_input_stream.offset := 0

    # Make sure that we do not overrun the buffer:
    if maximum_requested > memory_size
	maximum_requested := memory_size

    # Perform the read:
    unix_system:: unix_system := ??
    remaining :@= read@unix_system(unix_system,
      file_input_stream.file_descriptor_number,
      memory, 0, maximum_requested)
    file_input_stream.remaining := remaining

    # Remember if the end of stream has occured:
    if remaining = 0
	file_input_stream.done := true
    return remaining


procedure input_available@file_input_stream
    takes
	file_input_stream file_input_stream
    returns logical

    #: This procedure will return {true}@{logical} if there is at least
    #, one byte of input avaiable from {file_input_stream} and
    #, {false}@{logical} otherwise.  This procedure does not do read
    #, ahead, so it will not block.

    #system :@= standard@system()
    #debug_stream :@= system.error_out_stream
    #put@("=>input_available@file_input_stream()\n\", debug_stream)

    result:: logical := false
    if file_input_stream.remaining != 0
	result := true
    else
	unix_system :@= one_and_only@unix_system()
	file_set :@= file_input_stream.file_set
	if file_input_stream.file_set == ??
	    file_set :@= create@file_set()
	    file_input_stream.file_set := file_set
	clear@(file_set)
	enter@(file_set, file_input_stream.file_descriptor_number)
	count :@= select@(unix_system, file_set, ??, ??, 0, 1)
	assert unix_system.status = ok
	result := count != 0

    #format@format1[logical](debug_stream,
    #  "<=input_available@file_input_stream()=>%l%\n\", result)

    return result


procedure is_done@file_input_stream
    takes
	file_input_stream file_input_stream
    returns logical

    #: This procedure will return {true} if no more characters can
    #, be read from {file_input_stream}.

    return file_input_stream.done


procedure open@file_input_stream
    takes
	file_name file_name
    returns input_stream

    #: This procedure will return open {file_name} and return an
    #, associated {input_stream} object.

    return open_buffer@file_input_stream(file_name, 4098)


procedure open_buffer@file_input_stream
    takes
	file_name file_name
	buffer_size unsigned
    returns input_stream

    #: This procedure will return open {file_name} for reading and return
    #, an associated {input_stream} object with a backing buffer of size
    #, {buffer_size}.  ??@{input_stream} is returned if the open failed.
    #, An invocation to {status_get}@{unix_system} may give a more
    #, diagnostic reason.

    # Open the file.
    assert buffer_size != 0

    file_name_string :@= string_convert@(file_name)
    unix_system:: unix_system := ??

    file_descriptor_number :@= open@(unix_system, file_name_string, 0, 0)
    if unix_system.status != ok
	return ??
    input_stream :@=
      file_descriptor_bind@file_input_stream(file_descriptor_number,
      buffer_size, file_name, file_name_string)
    return input_stream


procedure memory_read@file_input_stream
    takes
	file_input_stream file_input_stream
	to_memory memory
	to_offset unsigned
	amount_requested unsigned
    returns unsigned

    #: This procedure will read up to {amount_requested} bytes
    #, from {file_input_stream} and store them into {to_memory} starting
    #, at {to_offset}.  This procedure will fail if {to_offset}
    #, greater than or equal to the size of {to_memory}.
    #, The actual number of bytes read in is returned.  A return
    #, value of 0 means that there are no more bytes/characters available
    #, from {file_input_stream} and it can be closed.

    # Ensure that there is enough space to satisfy the request:
    assert to_offset + amount_requested <= to_memory.size

    # Grab some more data if needed:
    remaining :@= file_input_stream.remaining
    if remaining = 0
	remaining := fill@(file_input_stream, amount_requested)
    else_if amount_requested < remaining
	remaining := amount_requested
    assert remaining <= amount_requested

    # Perform the transfer:
    from_memory :@= file_input_stream.memory
    from_offset :@= file_input_stream.offset
    transfer@(from_memory, from_offset, to_memory, to_offset, remaining)

    # Update the buffer information:
    file_input_stream.offset :+= remaining
    file_input_stream.remaining :-= remaining
    return remaining


procedure string_append@file_input_stream
    takes
	file_input_stream file_input_stream
	buffer string
	amount_requested unsigned
    returns unsigned

    #: This procedure will append up to {amount_requested} characters
    #, from {file_input_stream} to {buffer}.  The actual number of characters
    #, appended is returned.  A return value of 0 means that there are
    #, no more characters available from {file_input_stream} and it can be
    #, closed.

    remaining :@= file_input_stream.remaining
    if remaining = 0
	remaining :@= fill@(file_input_stream, amount_requested)
    else_if amount_requested < remaining
	remaining := amount_requested
    assert remaining <= amount_requested

    # Perform the transfer:
    memory :@= file_input_stream.memory
    offset :@= file_input_stream.offset
    index :@= 0
    loop
	while index < remaining
	character_append@(buffer, character_convert@(memory[offset + index]))
	index :+= 1

    file_input_stream.offset := offset + remaining
    file_input_stream.remaining :-= remaining
    return remaining


# Polymorphic implementation using co-types:
#
#define input_stream[buffer]
#    down_type input_stream
#    needs
#	procedure read
#	    takes buffer, unsigned, unsigned
#	    returns unsigned
#	procedure close
#	    takes buffer
#	    returns_nothing
#    record
#	buffer buffer
#	offset unsigned
#	unread unsigned
#    generate allocate, down_convert, erase, print, up_convert
#
#procedure create@input_stream[buffer]
#    takes
#	buffer buffer
#    returns input_stream
#
#    input_stream1 :@= new@input_stream1[buffer]()
#    input_stream1.buffer :@= buffer
#    input_stream :@= down_convert@(input_stream1)
#    return input_stream
#
#procedure read@input_stream
#    up_type input_stream[buffer]
#    takes
#	input_stream input_stream
#	amount unsigned
#    returns unsigned
#
#    input_stream1 :@= up_convert@(input_stream)
#    result :@= read@(input_stream1.buffer,
#      input_stream.offset + input_stream.unread, offset)
#    input_stream.unread :+= result
#    return result
#
#procedure close@input_stream
#    up_type input_stream[buffer]
#    takes
#	input_stream input_stream
#    returns_nothing
#
#    input_stream1 :@= up_convert@(input_stream)
#    close@(input_stream)

