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

///|
priv struct Parser {
  args : Array[String]
  mut pos : Int
}

///|
fn Parser::peek(self : Parser) -> String? {
  self.args.get(self.pos)
}

///|
fn Parser::take(self : Parser) -> String raise TestError {
  guard self.args.get(self.pos) is Some(value) else {
    raise TestError("missing argument")
  }
  self.pos += 1
  value
}

///|
fn is_unary(op : String) -> Bool {
  op == "-n" ||
  op == "-z" ||
  op == "-e" ||
  op == "-a" ||
  op == "-f" ||
  op == "-d" ||
  op == "-r" ||
  op == "-w" ||
  op == "-x"
}

///|
fn is_binary(op : String) -> Bool {
  op == "=" ||
  op == "==" ||
  op == "!=" ||
  op == "<" ||
  op == ">" ||
  op == "-eq" ||
  op == "-ne" ||
  op == "-lt" ||
  op == "-le" ||
  op == "-gt" ||
  op == "-ge"
}

///|
async fn eval_unary(op : String, value : String) -> Bool {
  match op {
    "-n" => value != ""
    "-z" => value == ""
    "-e" | "-a" => @fs.exists(value)
    "-f" => (@fs.kind(value) catch { _ => Unknown }) == Regular
    "-d" => (@fs.kind(value) catch { _ => Unknown }) == Directory
    "-r" => @fs.can_read(value)
    "-w" => @fs.can_write(value)
    "-x" => @fs.can_execute(value)
    _ => raise TestError("unknown unary operator: '\{op}'")
  }
}

///|
fn parse_integer(text : String) -> Int64 raise TestError {
  @string.parse_int64(text) catch {
    _ => raise TestError("integer expression expected: '\{text}'")
  }
}

///|
fn eval_binary(
  left : String,
  op : String,
  right : String,
) -> Bool raise TestError {
  match op {
    "=" | "==" => left == right
    "!=" => left != right
    "<" => left < right
    ">" => left > right
    "-eq" => parse_integer(left) == parse_integer(right)
    "-ne" => parse_integer(left) != parse_integer(right)
    "-lt" => parse_integer(left) < parse_integer(right)
    "-le" => parse_integer(left) <= parse_integer(right)
    "-gt" => parse_integer(left) > parse_integer(right)
    "-ge" => parse_integer(left) >= parse_integer(right)
    _ => raise TestError("unknown binary operator: '\{op}'")
  }
}

///|
async fn Parser::parse_primary(self : Parser) -> Bool {
  let token = self.take()
  if token == "(" {
    let value = self.parse_or()
    if self.take() != ")" {
      raise TestError("expected ')'")
    }
    return value
  }
  if is_unary(token) {
    return eval_unary(token, self.take())
  }
  match self.peek() {
    Some(op) if is_binary(op) => {
      ignore(self.take())
      eval_binary(token, op, self.take())
    }
    _ => token != ""
  }
}

///|
async fn Parser::parse_not(self : Parser) -> Bool {
  if self.peek() == Some("!") {
    ignore(self.take())
    !self.parse_not()
  } else {
    self.parse_primary()
  }
}

///|
async fn Parser::parse_and(self : Parser) -> Bool {
  let mut value = self.parse_not()
  while self.peek() == Some("-a") {
    ignore(self.take())
    let right = self.parse_not()
    value = value && right
  }
  value
}

///|
async fn Parser::parse_or(self : Parser) -> Bool {
  let mut value = self.parse_and()
  while self.peek() == Some("-o") {
    ignore(self.take())
    let right = self.parse_and()
    value = value || right
  }
  value
}

///|
async fn main {
  let args = @env.args()[1:].to_owned()
  if args == ["--help"] {
    @stdio.stdout.write(
      "Usage: test EXPRESSION\nSupports string, integer, file, !, -a, and -o expressions.\n",
    )
    return
  }
  if args.is_empty() {
    @sys.exit(1)
    return
  }
  let parser : Parser = { args, pos: 0, }
  let result = parser.parse_or() catch {
    TestError(message) => {
      @stdio.stderr.write("test: \{message}\n")
      @sys.exit(2)
      return
    }
    err => {
      @stdio.stderr.write("test: \{err}\n")
      @sys.exit(2)
      return
    }
  }
  if parser.pos != args.length() {
    @stdio.stderr.write("test: unexpected argument: '\{args[parser.pos]}'\n")
    @sys.exit(2)
    return
  }
  if !result {
    @sys.exit(1)
  }
}