///|
fn is_space(unit : UInt16) -> Bool {
  unit == 32 || (unit >= 9 && unit <= 13)
}

///|
fn normalize(source : String) -> String {
  let out = StringBuilder()
  let mut start = 0
  let mut pending_space = false
  for i = 0; i <= source.length(); i = i + 1 {
    if i == source.length() || is_space(source.code_unit_at(i)) {
      if i > start {
        if pending_space {
          out.write_string(" ")
        }
        out.write_string(source[start:i].to_owned())
        pending_space = true
      }
      start = i + 1
    }
  }
  out.to_string()
}

///|
fn starts_word(text : String, word : String) -> Bool {
  starts_word_at(text, 0, word)
}

///|
fn starts_word_at(text : String, at : Int32, word : String) -> Bool {
  let end = at + word.length()
  text.length() >= end &&
  text[at:end].to_owned().to_lower() == word &&
  (text.length() == end || text.code_unit_at(end) == 32)
}

///|
fn clause_keyword(text : String) -> String? {
  for word in ["where", "while", "when", "if", "then", "the"] {
    if starts_word(text, word) {
      return Some(word)
    }
  }
  None
}

///|
fn starts_then_system(text : String, at : Int32) -> Bool {
  starts_word_at(text, at, "then") && starts_word_at(text, at + 5, "the")
}

///|
// A comma is structural only when a reserved clause keyword follows it.
// Ordinary commas in natural-language clauses remain part of that clause.
fn take_clause(
  text : String,
  unwanted : Bool,
) -> (String, String) raise ParseError {
  let mut saw_incomplete_then = false
  for i in 0.. Requirement raise ParseError {
  let text = normalize(source)
  guard !text.is_empty() else { raise EmptyRequirement }
  let mut rest = text
  let mut feature = None
  let states = []
  let mut trigger = None
  while !starts_word(rest, "the") {
    guard clause_keyword(rest) is Some(keyword) else { raise ExpectedSystem }
    match keyword {
      "where" => {
        guard feature is None && states.is_empty() && trigger is None else {
          raise InvalidClauseOrder
        }
      }
      "while" | "when" | "if" => {
        guard trigger is None else { raise InvalidClauseOrder }
      }
      _ => raise InvalidClauseOrder
    }
    let payload = rest[keyword.length():].trim_start().to_owned()
    let (clause, next) = take_clause(payload, keyword == "if")
    guard !clause.is_empty() else { raise EmptyClause(keyword~) }
    match keyword {
      "where" => feature = Some(clause)
      "while" => states.push(clause)
      "when" => trigger = Some(Event(clause))
      "if" => trigger = Some(Unwanted(clause))
      _ => ()
    }
    rest = if keyword == "if" {
      guard starts_word(next, "then") else { raise MissingThen }
      let after_then = next[4:].trim_start().to_owned()
      guard starts_word(after_then, "the") else { raise ExpectedSystem }
      after_then
    } else {
      next
    }
  }
  let body = rest[3:].trim_start().to_owned()
  let mut shall_at = None
  for i = 0; i < body.length(); i = i + 1 {
    if (i == 0 || body.code_unit_at(i - 1) == 32) &&
      starts_word_at(body, i, "shall") {
      shall_at = Some(i)
      break
    }
  }
  guard shall_at is Some(at) else { raise MissingShall }
  let system = body[:at].trim_end().to_owned()
  guard !system.is_empty() else { raise ExpectedSystem }
  let response = body[at + 5:].trim_start().to_owned()
  guard !response.is_empty() && response != "." else { raise MissingResponse }
  {
    feature,
    preconditions: ReadOnlyArray::from_array(states[:]),
    trigger,
    system,
    response,
  }
}

///|
pub fn parse(source : String) -> Requirement raise ParseError {
  Requirement::Requirement(source)
}