///|
/// An open input: a file, or stdin. `read(n)` gives at most `n` bytes, and no
/// bytes at end of input. `close` releases a file; stdin stays open.
struct InputSource {
  read : (Int32) -> Bytes raise
  close : () -> Unit raise
}

///|
/// The most bytes that one read of a streamed input asks for: 64 KiB.
const INPUT_CHUNK_BYTES : Int32 = 65536

///|
/// The name of `path` in error messages.
fn input_name(path : String) -> String {
  if path == "-" {
    "stdin"
  } else {
    "'\{path}'"
  }
}

///|
/// How `read_chunks` ended.
priv enum ChunksEnd {
  /// The input ended, and the source closed.
  Ended
  /// The first read failed before any byte arrived, so the input was never
  /// readable. The caller reports it like an input that did not open. The
  /// source is closed.
  Unreadable(Error)
  /// A read failed after some bytes arrived, or the close failed. The
  /// source is closed.
  Failed(Error)
}

///|
/// Gives each chunk of `source` to `consume` until end of input, then closes
/// the source. A read or close error is given back, not raised, so that the
/// caller can finish the work on the chunks before it. An error that
/// `consume` raises closes the source and propagates.
fn read_chunks(
  source : InputSource,
  consume : (Bytes) -> Unit raise,
) -> ChunksEnd raise {
  let mut open = true
  let mut started = false
  errdefer (if open { (source.close)() catch { _ => () } })
  while true {
    let chunk = (source.read)(INPUT_CHUNK_BYTES) catch {
      error => {
        open = false
        (source.close)() catch {
          _ => ()
        }
        return if started { Failed(error) } else { Unreadable(error) }
      }
    }
    if chunk.is_empty() {
      break
    }
    started = true
    consume(chunk)
  }
  open = false
  (source.close)() catch {
    error => return Failed(error)
  }
  Ended
}

///|
/// Reads all of `path`, or of stdin for "-".
fn read_input_bytes(path : String) -> Bytes raise {
  let bytes : Array[Byte] = []
  let failure = read_chunks(chunk_source(path), chunk => {
    for byte in chunk {
      bytes.push(byte)
    }
  })
  if failure is (Unreadable(error) | Failed(error)) {
    raise error
  }
  Bytes::from_array(bytes)
}

///|
/// A source over bytes already in memory: the js and wasm-gc binaries read
/// the whole input before they check it. Each read gives the next slice.
#cfg(any(target="js", target="wasm-gc"))
fn whole_source(bytes : Bytes) -> InputSource {
  let mut offset = 0
  {
    read: requested => {
      let end = Int32::min(bytes.length(), offset + requested)
      let chunk = bytes[offset:end].to_owned()
      offset = end
      chunk
    },
    close: () => (),
  }
}