///|
/// The tokens of `node` that are not whitespace or line breaks.
fn significant(node : @cst.Node) -> Array[@cst.Token] {
  node.tokens().filter(t => t.kind != Whitespace && t.kind != Newline)
}

///|
fn texts_of(tokens : Array[@cst.Token], kind : @cst.TokenKind) -> Array[String] {
  tokens.filter(t => t.kind == kind).map(t => t.text)
}

///|
fn first_text(tokens : Array[@cst.Token]) -> String {
  match tokens.iter().find_first(t => t.kind == Text) {
    Some(t) => t.text
    None => ""
  }
}

///|
fn has_keyword(tokens : Array[@cst.Token], word : String) -> Bool {
  tokens.iter().any(t => t.kind == Keyword && t.text.to_lower() == word)
}

///|
fn trim(s : String) -> String {
  s.trim(chars=" \t\r\n").to_owned()
}

///|
fn frame_kind(word : String) -> FrameKind? {
  match word {
    "loop" => Some(Loop)
    "rect" => Some(Rect)
    "opt" => Some(Opt)
    "alt" => Some(Alt)
    "par" => Some(Par)
    "par_over" => Some(ParOver)
    "critical" => Some(Critical)
    "break" => Some(Break)
    _ => None
  }
}

///|
/// The statement of one CST statement node, or `None` for a node that is not
/// a statement, such as an error node.
fn statement(source : String, node : @cst.Node) -> Statement? {
  let tokens = significant(node)
  guard tokens.last() is Some(last) else { return None }
  let span = @cst.span(source, tokens[0].offset, last.offset + last.text.length())
  let names = texts_of(tokens, Identifier)
  let kind : StatementKind = match node.kind {
    Participant => {
      let keyword : ParticipantKeyword = if has_keyword(tokens, "actor") {
        Actor
      } else {
        Participant
      }
      let config = match tokens.iter().find_first(t => t.kind == Config) {
        Some(t) => Some(trim(t.text))
        None => None
      }
      let label = if has_keyword(tokens, "as") {
        Some(first_text(tokens))
      } else {
        None
      }
      Participant({
        created: has_keyword(tokens, "create"),
        keyword,
        id: names[0],
        config,
        label,
      })
    }
    Destroy => Destroy(names[0])
    Activate => Activate(names[0])
    Deactivate => Deactivate(names[0])
    Message => {
      let arrow_at = match tokens.search_by(t => t.kind == Arrow) {
        Some(i) => i
        None => return None
      }
      let mut activation : Activation? = None
      let mut before = false
      let mut after = false
      for i, t in tokens {
        if t.kind == Modifier {
          if i < arrow_at {
            before = true
          } else if t.text == "()" {
            after = true
          } else {
            activation = Some(if t.text == "+" { Activate } else { Deactivate })
          }
        }
      }
      Message({
        sender: names[0],
        arrow: tokens[arrow_at].text,
        receiver: names[1],
        text: first_text(tokens),
        activation,
        central_before: before,
        central_after: after,
      })
    }
    Note => {
      let placement : Placement = if has_keyword(tokens, "over") {
        Over
      } else if has_keyword(tokens, "left of") {
        LeftOf
      } else {
        RightOf
      }
      Note({
        placement,
        participants: ReadOnlyArray::from_array(names),
        text: first_text(tokens),
      })
    }
    Link | Links | Properties | Details => {
      let kind : LinkKind = match node.kind {
        Link => Link
        Links => Links
        Properties => Properties
        _ => Details
      }
      Link({ kind, participant: names[0], text: first_text(tokens) })
    }
    Title => Title(first_text(tokens))
    AccTitle => AccTitle(first_text(tokens))
    AccDescr => AccDescr(trim(first_text(tokens)))
    Autonumber => {
      let numbers = texts_of(tokens, Number)
      if has_keyword(tokens, "off") {
        Autonumber(Off)
      } else {
        Autonumber(On(start=numbers.get(0), step=numbers.get(1)))
      }
    }
    Block => {
      let parts = node.nodes()
      guard parts.length() > 0 && parts[0].kind == BlockStart else { return None }
      let opener = significant(parts[0])
      let word = opener[0].text.to_lower()
      let sections : Array[(String, Array[Statement])] = [(first_text(opener), [])]
      for part in parts[1:] {
        match part.kind {
          Section => sections.push((first_text(significant(part)), []))
          BlockEnd => ()
          _ =>
            if statement(source, part) is Some(s) {
              sections[sections.length() - 1].1.push(s)
            }
        }
      }
      if word == "box" {
        Box({
          label: sections[0].0,
          statements: ReadOnlyArray::from_array(sections[0].1),
        })
      } else {
        guard frame_kind(word) is Some(kind) else { return None }
        Frame({
          kind,
          sections: ReadOnlyArray::from_array(
            sections.map(s => {
              label: s.0,
              statements: ReadOnlyArray::from_array(s.1),
            }),
          ),
        })
      }
    }
    _ => return None
  }
  Some({ kind, span })
}

///|
/// The typed AST of a lossless syntax tree. Error nodes are not in the AST,
/// because the parser already reported them. A tree without the
/// `sequenceDiagram` header gives `None`.
pub fn lower(tree : @cst.Node) -> Diagram? {
  let source = tree.text()
  let nodes = tree.nodes()
  guard nodes.iter().any(n => n.kind == Header) else { return None }
  let statements = []
  for node in nodes {
    if statement(source, node) is Some(s) {
      statements.push(s)
    }
  }
  let front_matter = match
    tree.tokens().iter().find_first(t => t.kind == FrontMatter) {
    Some(t) => Some(t.text)
    None => None
  }
  let directives = texts_of(tree.tokens(), Directive)
  Some(Diagram::new(statements, front_matter?, directives~))
}