///|
priv suberror ModeError {
  InvalidMode
  NeedsUmask
}

///|
priv struct ModeChange {
  who : Int32
  op : Char
  value : Int32
  copy : Int32?
  mentioned : Int32
}

///|
fn compile_mode(text : String) -> Array[ModeChange] raise ModeError {
  let chars = text.iter().collect()
  let changes : Array[ModeChange] = []
  let mut index = 0
  while index < chars.length() {
    let mut who = 0
    while index < chars.length() && chars[index] is ('u' | 'g' | 'o' | 'a') {
      who = who |
        (match chars[index] {
          'u' => 0o4700
          'g' => 0o2070
          'o' => 0o1007
          _ => 0o7777
        })
      index += 1
    }
    let before = changes.length()
    while index < chars.length() && chars[index] is ('+' | '-' | '=') {
      let op = chars[index]
      index += 1
      let mut value = 0
      let mut mentioned = 0
      let mut copy : Int32? = None
      let start = index
      while index < chars.length() && !(chars[index] is ('+' | '-' | '=' | ',')) {
        match chars[index] {
          'r' => value = value | 0o444
          'w' => value = value | 0o222
          'x' | 'X' => value = value | 0o111
          's' => {
            value = value | 0o6000
            mentioned = mentioned | 0o6000
          }
          't' => {
            value = value | 0o1000
            mentioned = mentioned | 0o1000
          }
          'u' => copy = Some(6)
          'g' => copy = Some(3)
          'o' => copy = Some(0)
          _ => raise InvalidMode
        }
        index += 1
      }
      if copy is Some(_) && index - start != 1 {
        raise InvalidMode
      }
      changes.push({ who, op, value, copy, mentioned, })
    }
    if changes.length() == before {
      raise InvalidMode
    }
    if index < chars.length() {
      if chars[index] != ',' {
        raise InvalidMode
      }
      index += 1
      if index == chars.length() {
        raise InvalidMode
      }
    }
  }
  if changes.is_empty() {
    raise InvalidMode
  }
  changes
}

///|
fn adjust_mode(changes : ArrayView[ModeChange], mask : Int32) -> Int32 {
  let mut mode = 0o777
  let mut mode_bits = 0
  for change in changes {
    let omit = 0o6000 & change.mentioned.lnot()
    let value = if change.copy is Some(shift) {
      let bits = (mode >> shift) & 7
      bits | (bits << 3) | (bits << 6)
    } else {
      change.value
    }
    let affected = if change.who == 0 { mask.lnot() } else { change.who }
    let value = value & affected & omit.lnot()
    mode = match change.op {
      '=' => {
        let preserved = (if change.who == 0 { 0 } else { change.who.lnot() }) |
          omit
        mode_bits = mode_bits | (0o7777 & preserved.lnot())
        (mode & preserved) | value
      }
      '+' => {
        mode_bits = mode_bits | value
        mode | value
      }
      _ => {
        mode_bits = mode_bits | value
        mode & value.lnot()
      }
    }
  }
  let mode = mode & 0o7777
  // GNU creates special-mode directories without group/other write access,
  // then applies chmod only when a requested bit differs from the new inode.
  let created = if ((mode_bits & 0o6000) | (mode & 0o1000)) != 0 {
    mode & (0o6022).lnot()
  } else {
    mode
  }
  if ((created ^ mode) & mode_bits) != 0 {
    mode
  } else {
    created
  }
}

///|
fn symbolic_mode(text : String) -> Int32 raise ModeError {
  let changes = compile_mode(text)
  let result = adjust_mode(changes, 0)
  if changes.any(change => change.who == 0) {
    // Establish independence from the unknown umask, rather than guessing it.
    for mask in 1..<512 {
      if adjust_mode(changes, mask) != result {
        raise NeedsUmask
      }
    }
  }
  result
}