///|
priv suberror CliError {
  CliError(String)
}

///|
/// `Last(n)` selects the final n units; `FromStart(k)` selects from the k-th
/// unit (1-based) to the end, mirroring tail's `-n +K` syntax.
priv enum CountSpec {
  Last(Int64)
  FromStart(Int64)
}

///|
let chunk_size : Int = 65536

///|
fn tail_command() -> @argparse.Command {
  Command(
    "tail",
    about="Print the last lines (or bytes) of each input.",
    flags=[
      FlagArg("quiet", short='q', about="Never print file name headers."),
      FlagArg("verbose", short='v', about="Always print file name headers."),
    ],
    options=[
      OptionArg(
        "lines",
        short='n',
        about="Print the last N lines, or from line +N (default 10).",
        allow_hyphen_values=true,
      ),
      OptionArg(
        "bytes",
        short='c',
        about="Print the last N bytes, or from byte +N.",
        conflicts_with=["lines"],
      ),
    ],
    positionals=[
      PositionArg(
        "files",
        about="[file...] ('-' or no file reads stdin)",
        num_args=ValueRange(lower=0),
      ),
    ],
    disable_help_subcommand=true,
  )
}

///|
fn option_value(matches : @argparse.Matches, name : String) -> String? {
  match matches.values.get(name) {
    Some(vals) =>
      if vals.is_empty() {
        None
      } else {
        Some(vals[vals.length() - 1])
      }
    None => None
  }
}

///|
fn parse_spec(text : String) -> CountSpec raise CliError {
  let (from_start, digits) = if text is ['+', .. rest] {
    (true, rest)
  } else {
    (false, text)
  }
  let n = @string.parse_int64(digits) catch {
    _ => raise CliError("tail: invalid count: '\{text}'")
  }
  if n < 0 {
    raise CliError("tail: invalid count: '\{text}'")
  }
  if from_start {
    FromStart(if n < 1 { 1 } else { n })
  } else {
    Last(n)
  }
}

///|
/// Select lines from an in-memory buffer (used for streamed input after the
/// ring buffer has trimmed it down). A trailing newline terminates the final
/// line rather than starting an empty one.
fn select_lines(data : Bytes, spec : CountSpec) -> BytesView {
  match spec {
    Last(n) => {
      if n <= 0 || data.length() == 0 {
        return data[0:0]
      }
      let mut i = if data[data.length() - 1] is b'\n' {
        data.length() - 2
      } else {
        data.length() - 1
      }
      let mut newlines : Int64 = 0
      while i >= 0 {
        if data[i] is b'\n' {
          newlines += 1
          if newlines == n {
            return data[i + 1:]
          }
        }
        i -= 1
      }
      data
    }
    FromStart(k) => {
      if k <= 1 {
        return data
      }
      let mut seen : Int64 = 0
      for i in 0.. BytesView {
  let len = data.length().to_int64()
  match spec {
    Last(n) => {
      let start = if n >= len { 0 } else { (len - n).to_int() }
      data[start:]
    }
    FromStart(k) => {
      let start = if k - 1 > len { data.length() } else { (k - 1).to_int() }
      data[start:]
    }
  }
}

///|
fn count_newlines(chunk : Bytes) -> Int {
  let mut n = 0
  for b in chunk {
    if b is b'\n' {
      n += 1
    }
  }
  n
}

///|
fn concat_chunks(chunks : Array[Bytes]) -> Bytes {
  let out : Array[Byte] = []
  for chunk in chunks {
    for b in chunk {
      out.push(b)
    }
  }
  Bytes::from_array(out)
}

///|
/// Stream file bytes in [start, end) to stdout in fixed-size chunks.
async fn stream_range(file : @fs.File, start : Int64, end : Int64) -> Unit {
  let mut pos = start
  while pos < end {
    let len = if end - pos < chunk_size.to_int64() {
      (end - pos).to_int()
    } else {
      chunk_size
    }
    @stdio.stdout.write(file.read_exactly_at(len, position=pos))
    pos += len.to_int64()
  }
}

///|
/// GNU-style tail for seekable files: find the start of the selection by
/// scanning backward (or forward for +K) in fixed-size chunks, then stream
/// it out. Memory stays bounded by the chunk size for any file size.
async fn tail_file(
  file : @fs.File,
  spec : CountSpec,
  bytes_mode : Bool,
) -> Unit {
  let size = file.size()
  if bytes_mode {
    let start : Int64 = match spec {
      Last(n) => if n >= size { 0 } else { size - n }
      FromStart(k) => if k - 1 > size { size } else { k - 1 }
    }
    stream_range(file, start, size)
    return
  }
  match spec {
    Last(n) => {
      if n <= 0 || size == 0 {
        return
      }
      let mut newlines : Int64 = 0
      let mut start : Int64 = 0
      let mut found = false
      let mut pos = size
      let mut at_file_end = true
      while pos > 0 && !found {
        let len = if pos < chunk_size.to_int64() {
          pos.to_int()
        } else {
          chunk_size
        }
        let chunk = file.read_exactly_at(len, position=pos - len.to_int64())
        let mut idx = chunk.length() - 1
        if at_file_end && chunk[idx] is b'\n' {
          // The trailing newline terminates the last line rather than
          // starting an empty one.
          idx -= 1
        }
        at_file_end = false
        while idx >= 0 {
          if chunk[idx] is b'\n' {
            newlines += 1
            if newlines == n {
              start = pos - len.to_int64() + idx.to_int64() + 1
              found = true
              break
            }
          }
          idx -= 1
        }
        pos -= len.to_int64()
      }
      stream_range(file, start, size)
    }
    FromStart(k) => {
      if k <= 1 {
        stream_range(file, 0, size)
        return
      }
      // Scan forward for the (k-1)-th newline, then stream the rest.
      let mut seen : Int64 = 0
      let mut pos : Int64 = 0
      while pos < size {
        let len = if size - pos < chunk_size.to_int64() {
          (size - pos).to_int()
        } else {
          chunk_size
        }
        let chunk = file.read_exactly_at(len, position=pos)
        for i in 0.. Bytes? {
  let n = r.read(buf)
  if n == 0 {
    None
  } else {
    Some(Bytes::from_array(buf[0:n]))
  }
}

///|
/// Tail an unseekable stream (stdin, pipes). `Last` keeps a ring of chunks
/// holding just the bytes of the selection, so memory is bounded by the
/// output size plus one chunk; `FromStart` writes through once the start
/// point has passed, buffering nothing.
async fn tail_stream(
  r : &@io.Reader,
  spec : CountSpec,
  bytes_mode : Bool,
) -> Unit {
  let buf : FixedArray[Byte] = FixedArray::make(chunk_size, 0)
  match spec {
    Last(n) => {
      if n <= 0 {
        // Drain the input like GNU tail, producing no output.
        while true {
          if read_chunk(r, buf) is None {
            break
          }
        }
        return
      }
      let chunks : Array[Bytes] = []
      let counts : Array[Int64] = []
      let mut total_newlines : Int64 = 0
      let mut total_bytes : Int64 = 0
      for ;; {
        match read_chunk(r, buf) {
          Some(chunk) => {
            chunks.push(chunk)
            total_bytes += chunk.length().to_int64()
            if bytes_mode {
              while chunks.length() > 1 &&
                    total_bytes - chunks[0].length().to_int64() >= n {
                total_bytes -= chunks[0].length().to_int64()
                ignore(chunks.remove(0))
              }
            } else {
              let c = count_newlines(chunk).to_int64()
              counts.push(c)
              total_newlines += c
              // Keep more than n newlines beyond the dropped prefix so the
              // last n lines stay fully buffered. (Written as `> n` rather
              // than `>= n + 1`, which would overflow for n = INT64_MAX.)
              while chunks.length() > 1 && total_newlines - counts[0] > n {
                total_newlines -= counts[0]
                ignore(chunks.remove(0))
                ignore(counts.remove(0))
              }
            }
          }
          None => break
        }
      }
      let data = concat_chunks(chunks)
      if bytes_mode {
        @stdio.stdout.write(select_bytes(data, spec))
      } else {
        @stdio.stdout.write(select_lines(data, spec))
      }
    }
    FromStart(k) =>
      if bytes_mode {
        let mut to_skip : Int64 = if k > 1 { k - 1 } else { 0 }
        for ;; {
          match read_chunk(r, buf) {
            Some(chunk) =>
              if to_skip >= chunk.length().to_int64() {
                to_skip -= chunk.length().to_int64()
              } else if to_skip > 0 {
                @stdio.stdout.write(chunk[to_skip.to_int():])
                to_skip = 0
              } else {
                @stdio.stdout.write(chunk)
              }
            None => break
          }
        }
      } else {
        let mut remaining : Int64 = if k > 1 { k - 1 } else { 0 }
        for ;; {
          match read_chunk(r, buf) {
            Some(chunk) => {
              if remaining == 0 {
                @stdio.stdout.write(chunk)
                continue
              }
              let mut emitted = false
              for i in 0.. break
          }
        }
      }
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let command = tail_command()
  let matches = command.parse(argv=args, env=Map([])) catch {
    err => {
      @stdio.stderr.write("\{err}\n")
      @sys.exit(2)
      return
    }
  }
  let quiet = matches.flags.get_or_default("quiet", false)
  let verbose = matches.flags.get_or_default("verbose", false)
  let (spec, bytes_mode) = try {
    match option_value(matches, "bytes") {
      Some(text) => (parse_spec(text), true)
      None =>
        match option_value(matches, "lines") {
          Some(text) => (parse_spec(text), false)
          None => (Last(10), false)
        }
    }
  } catch {
    CliError(msg) => {
      @stdio.stderr.write("\{msg}\n")
      @sys.exit(2)
      return
    }
  }
  let files = matches.values.get("files").unwrap_or([])
  let sources = if files.is_empty() { ["-"] } else { files }
  let show_headers = (sources.length() > 1 || verbose) && !quiet
  let mut failed = false
  for index, path in sources {
    if show_headers {
      let name = if path == "-" { "standard input" } else { path }
      let prefix = if index > 0 { "\n" } else { "" }
      @stdio.stdout.write("\{prefix}==> \{name} <==\n")
    }
    try {
      if path == "-" {
        tail_stream(@stdio.stdin, spec, bytes_mode)
      } else {
        let file = @fs.open(path, mode=ReadOnly)
        defer file.close()
        if file.kind() is Regular {
          tail_file(file, spec, bytes_mode)
        } else {
          // Named pipes and devices are not seekable; stream them instead.
          tail_stream(file, spec, bytes_mode)
        }
      }
    } catch {
      err => {
        @stdio.stderr.write("tail: \{err}\n")
        failed = true
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}