///|
priv suberror InvalidCRecord {
  InvalidCRecord
}

///|
priv struct RevOutput {
  writer : &@io.Writer
  buffer : FixedArray[Byte]
  mut length : Int32
  mut pending : (BytesView, Bool, Int32)?
  count : Ref[Int64]
  mut failed : Bool
}

///|
async fn RevOutput::flush(self : RevOutput) -> Unit {
  let length = self.length
  self.length = 0
  errdefer {
    self.failed = true
  }
  if length > 0 {
    self.writer.write(self.buffer.unsafe_reinterpret_as_bytes()[:length])
  }
}

///|
// Copy backwards into a fixed output buffer. Suspend only at a full buffer;
// count a record after its terminator is buffered, exactly as BufferedWriter.
fn RevOutput::append(
  self : RevOutput,
  data : BytesView,
  terminated : Bool,
  remaining : Int32,
) -> Bool {
  let copied = remaining.min(self.buffer.length() - self.length)
  let mut index = 0
  // A big-endian store reverses the eight bytes of a little-endian load.
  // Both accesses stay within this record view and the free output span.
  while copied - index >= 8 {
    self.buffer.unsafe_write_uint64_be(
      self.length + index,
      data.unsafe_read_uint64_le(remaining - index - 8),
    )
    index += 8
  }
  while index < copied {
    self.buffer[self.length + index] = data[remaining - index - 1]
    index += 1
  }
  self.length += copied
  let remaining = remaining - copied
  if remaining > 0 || (terminated && self.length == self.buffer.length()) {
    self.pending = Some((data, terminated, remaining))
    return false
  }
  if terminated {
    self.buffer[self.length] = b'\n'
    self.length += 1
  }
  self.count.val += 1L
  self.pending = None
  true
}

///|
fn invalid_c_record(data : BytesView) -> Bool {
  let mut index = 0
  while data.length() - index >= 8 {
    if (data.unsafe_read_uint64_le(index) & 0x8080808080808080UL) != 0UL {
      return true
    }
    index += 8
  }
  while index < data.length() {
    if data[index].to_int() >= 128 {
      return true
    }
    index += 1
  }
  false
}

///|
fn help_message() -> String {
  let message =
    #|Usage: rev [OPTION]... [FILE]...
    #|      --help      display this help and exit
    #|      --version   display version and exit
    #|
    #|Reverse each line from ordered FILE operands, or standard input if absent.
    #|The fixed Linux C-locale profile reverses bytes and rejects non-ASCII records.
    #|An explicit - is a filename, as in the frozen util-linux profile.
    #|Input streams one record at a time; memory follows the longest record.
  message
}

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("rev 0.1.6\n")
    return
  }
  let parsed = @cli.parse(@env.args()[1:], [@cli.flag("help")]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("rev: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(help_message() + "\n")
    return
  }
  if parsed.operands.is_empty() {
    let lines = Ref(0L)
    reverse_reader(@stdio.stdin, lines) catch {
      error => {
        @stdio.stderr.write(
          "rev: stdin: " +
          lines.val.to_string() +
          ": " +
          error_message(error) +
          "\n",
        )
        @sys.exit(1)
      }
    }
    return
  }
  let mut status = 0
  for path in parsed.operands {
    let mut opened = false
    let lines = Ref(0L)
    try {
      let file = @fs.open(path, mode=ReadOnly)
      opened = true
      defer file.close()
      if file.kind() == @fs.FileKind::Directory {
        @stdio.stderr.write("rev: " + path + ": 0: Is a directory\n")
        status = 1
      } else {
        reverse_reader(file, lines)
      }
    } catch {
      error => {
        let prefix = if opened {
          "rev: " + path + ": " + lines.val.to_string() + ": "
        } else {
          "rev: cannot open " + path + ": "
        }
        @stdio.stderr.write(prefix + error_message(error) + "\n")
        status = 1
      }
    }
  }
  if status != 0 {
    @sys.exit(status)
  }
}

///|
async fn reverse_reader(
  reader : &@io.Reader,
  count : Ref[Int64],
  writer~ : &@io.Writer = @stdio.stdout,
) -> Unit {
  let lines = @stream.RecordReader::new(reader)
  let output : RevOutput = {
    writer,
    buffer: FixedArray::make(65536, b'\x00'),
    length: 0,
    pending: None,
    count,
    failed: false,
  }
  let mut invalid = false
  let consume = (data : BytesView, terminated : Bool) => {
    if invalid_c_record(data) {
      invalid = true
      false
    } else {
      output.append(data, terminated, data.length())
    }
  }
  let processing_error = try {
    while lines.next_batch() is Some(batch) {
      while !batch.each_while(consume) {
        if invalid {
          raise InvalidCRecord
        }
        output.flush()
        while output.pending is Some((data, terminated, remaining)) {
          if !output.append(data, terminated, remaining) {
            output.flush()
          }
        }
      }
    }
    None
  } catch {
    error => Some(error)
  }
  if !output.failed {
    output.flush()
  }
  if processing_error is Some(error) {
    raise error
  }
}

///|
fn error_message(error : Error) -> String {
  match error {
    InvalidCRecord => "Invalid or incomplete multibyte or wide character"
    @os_error.OSError(code, ..) as error =>
      if error.is_ENOENT() {
        "No such file or directory"
      } else if error.is_EACCES() {
        "Permission denied"
      } else if error.is_ENOTDIR() {
        "Not a directory"
      } else if !@platform.is_windows() && code == 21 {
        "Is a directory"
      } else if !@platform.is_windows() && code == 9 {
        "Bad file descriptor"
      } else {
        error.to_string()
      }
    _ => error.to_string()
  }
}