///|
fn parse_mode(text : String) -> Int raise {
  let digits = if text.has_prefix("0o") || text.has_prefix("0O") {
    text[2:].to_owned()
  } else {
    text
  }
  if digits == "" {
    fail("empty mode")
  }
  let mut value = 0
  for digit in digits {
    if !(digit is ('0'..='7')) {
      fail("invalid mode")
    }
    value = value * 8 + digit.to_int() - '0'.to_int()
    if value > 0o7777 {
      fail("mode out of range")
    }
  }
  value
}

///|
fn mode_text(mode : Int) -> String {
  let digits = "01234567"
  let out = StringBuilder()
  out.write_char('0')
  let first_shift = if mode > 0o777 { 0 } else { 1 }
  for shift in first_shift..<=3 {
    let value = (mode >> ((3 - shift) * 3)) & 0x7
    out.write_char(digits.get_char(value).unwrap())
  }
  out.to_string()
}

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

///|
fn parse_symbolic_mode(text : String) -> Int raise ChmodError {
  if text == "" {
    raise ChmodError("empty symbolic mode")
  }
  let mut mode = 0
  let mut user = false
  let mut group = false
  let mut other = false
  for clause_view in text.split(",") {
    let clause = clause_view.to_owned()
    guard clause.find("=") is Some(equal) else {
      raise ChmodError("symbolic mode must use '='")
    }
    if clause[equal + 1:].contains("=") || equal == 0 {
      raise ChmodError("invalid symbolic assignment: '\{clause}'")
    }
    let mut clause_user = false
    let mut clause_group = false
    let mut clause_other = false
    for who in clause[:equal] {
      match who {
        'u' => clause_user = true
        'g' => clause_group = true
        'o' => clause_other = true
        'a' => {
          clause_user = true
          clause_group = true
          clause_other = true
        }
        _ => raise ChmodError("invalid symbolic class: '\{who}'")
      }
    }
    let mut permissions = 0
    for permission in clause[equal + 1:] {
      match permission {
        'r' => permissions = permissions | 4
        'w' => permissions = permissions | 2
        'x' => permissions = permissions | 1
        _ =>
          raise ChmodError("unsupported symbolic permission: '\{permission}'")
      }
    }
    if clause_user {
      user = true
      mode = (mode & (0o700).lnot()) | (permissions << 6)
    }
    if clause_group {
      group = true
      mode = (mode & (0o070).lnot()) | (permissions << 3)
    }
    if clause_other {
      other = true
      mode = (mode & (0o007).lnot()) | permissions
    }
  }
  if !user || !group || !other {
    raise ChmodError("symbolic mode must explicitly assign u, g, and o")
  }
  mode
}

///|
async fn chmod_numeric_path(
  path : String,
  mode : Int,
  recursive : Bool,
  verbose : Bool,
) -> Unit {
  match @fs.kind(path, follow_symlink=false) {
    SymLink => raise ChmodError("refusing to chmod symbolic link: \{path}")
    Directory => {
      @fs.chmod(path, mode)
      if verbose {
        @stdio.stdout.write("mode of '\{path}' changed to \{mode_text(mode)}\n")
      }
      if recursive {
        let entries = @fs.readdir(
          path,
          include_hidden=true,
          include_special=false,
          sort=true,
        )
        for entry in entries {
          chmod_numeric_path(
            @path.Path(path).join(Path(entry)).to_string(),
            mode,
            true,
            verbose,
          )
        }
      }
    }
    _ => {
      @fs.chmod(path, mode)
      if verbose {
        @stdio.stdout.write("mode of '\{path}' changed to \{mode_text(mode)}\n")
      }
    }
  }
}

///|
async fn main {
  let args = @env.args()[1:]
  let parsed = @cli.parse(args, [
    @cli.flag("recursive", short='R'),
    @cli.flag("verbose", short='v'),
    @cli.option("reference"),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("chmod: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: chmod [-R] [-v] MODE FILE...\n       chmod [-R] [-v] --reference=RFILE FILE...\nMODE may be numeric or a complete symbolic '=' assignment such as a=rwx or u=rw,g=r,o=.\n",
    )
    return
  }
  if parsed.contains("reference") {
    if parsed.operands.is_empty() {
      @stdio.stderr.write("chmod: missing operand\n")
      @sys.exit(1)
      return
    }
    @stdio.stderr.write(
      "chmod: --reference is unavailable: the portable filesystem API cannot read permission bits\n",
    )
    @sys.exit(1)
    return
  }
  let (selected_mode, symbolic_mode) = match parsed.operands.get(0) {
    Some(value) => {
      let numeric = try parse_mode(value) catch {
        _ => None
      } noraise {
        mode => Some(mode)
      }
      match numeric {
        Some(mode) => (mode, false)
        None =>
          (
            parse_symbolic_mode(value) catch {
              ChmodError(message) => {
                @stdio.stderr.write(
                  "chmod: invalid mode '\{value}': \{message}\n",
                )
                @sys.exit(2)
                return
              }
            },
            true,
          )
      }
    }
    None => {
      @stdio.stderr.write("chmod: missing mode\n")
      @sys.exit(2)
      return
    }
  }
  let operands = parsed.operands[1:]
  if operands.is_empty() {
    @stdio.stderr.write("chmod: missing operand\n")
    @sys.exit(2)
    return
  }
  if !@platform.permission_mutation_supported() {
    @stdio.stderr.write("chmod: unsupported capability: permission mutation\n")
    @sys.exit(1)
    return
  }
  let verbose = parsed.contains("verbose")
  let mut failed = false
  if symbolic_mode {
    for path in operands {
      let kind = @fs.kind(path, follow_symlink=false) catch {
        err => {
          @stdio.stderr.write("chmod: cannot inspect '\{path}': \{err}\n")
          failed = true
          continue
        }
      }
      if kind != Regular {
        @stdio.stderr.write(
          "chmod: symbolic assignments are limited to regular files: '\{path}'\n",
        )
        failed = true
      }
    }
    if failed {
      @sys.exit(1)
      return
    }
    for path in operands {
      @fs.chmod(path, selected_mode) catch {
        err => {
          @stdio.stderr.write("chmod: cannot change mode: \{err}\n")
          failed = true
        }
      }
      if !failed && verbose {
        @stdio.stdout.write(
          "mode of '\{path}' changed to \{mode_text(selected_mode)}\n",
        )
      }
    }
  } else {
    for path in operands {
      chmod_numeric_path(
        path,
        selected_mode,
        parsed.contains("recursive"),
        verbose,
      ) catch {
        err => {
          @stdio.stderr.write("chmod: cannot change mode: \{err}\n")
          failed = true
        }
      }
    }
  }
  if failed {
    @sys.exit(1)
  }
}