/// The reader of the GML graphs of the upstream.
///
/// This is the port of the GML branch of `dowhy.causal_graph.CausalGraph`,
/// which reads a graph with `networkx.parse_gml`. The upstream accepts a GML
/// string, a DOT string, a networkx graph and a probabilistic causal model;
/// the port accepts a GML string and a graph that the caller has built.
///
/// The reader understands the subset of GML the upstream writes and reads: a
/// `graph` block that holds `node` blocks and `edge` blocks. A node is named
/// after its `label` attribute when it has one, and after its `id` attribute
/// otherwise, which is the convention of `networkx.read_gml` with its default
/// `label="label"`. An edge joins the node of its `source` attribute to the
/// node of its `target` attribute.
///
/// The upstream wraps the graph it reads in a `networkx.DiGraph`, so a GML
/// graph that is not directed is directed in the end. The graph of the port is
/// always directed.

///|
/// The graph of a GML string.
///
/// The graph is directed, like the `networkx.DiGraph` the upstream builds.
///
/// The edges of a GML graph name the `id` of their nodes, while the name of a
/// node is its `label`, so the edges are resolved with the labels of the nodes
/// the way `networkx.parse_gml` resolves them.
pub fn parse_gml(text : String) -> DirectedGraph {
  let graph = DirectedGraph::new()
  let tokens = tokenize_gml(text)
  let names : Map[String, String] = Map([])
  let mut position = 0
  while position < tokens.length() {
    let token = tokens[position]
    if token.is_word("node") {
      position = position + 1
      let attributes = read_gml_block(tokens, position)
      position = attributes.next
      let name = match attributes.label {
        Some(label) => label
        None =>
          match attributes.id {
            Some(id) => id
            None => ""
          }
      }
      if name != "" {
        graph.add_node(name)
        match attributes.id {
          Some(id) => names.set(id, name)
          None => ()
        }
      }
      continue
    }
    if token.is_word("edge") {
      position = position + 1
      let attributes = read_gml_block(tokens, position)
      position = attributes.next
      match (attributes.source, attributes.target) {
        (Some(source), Some(target)) =>
          graph.add_edge(
            resolve_gml_name(names, source),
            resolve_gml_name(names, target),
          )
        _ => ()
      }
      continue
    }
    position = position + 1
  }
  graph
}

///|
/// The name of the node with the given id.
///
/// A node whose id is not known keeps the id as its name, which is what a
/// graph whose edges name a node that is not declared holds.
fn resolve_gml_name(names : Map[String, String], id : String) -> String {
  match names.get(id) {
    Some(name) => name
    None => id
  }
}

///|
/// The attributes of a `node` or an `edge` block of a GML string.
priv struct GmlAttributes {
  /// The position of the token after the block.
  mut next : Int32
  /// The `id` attribute.
  mut id : String?
  /// The `label` attribute.
  mut label : String?
  /// The `source` attribute.
  mut source : String?
  /// The `target` attribute.
  mut target : String?
}

///|
/// The attributes of a block, read from the tokens that follow the opening
/// bracket of the block.
fn read_gml_block(tokens : Array[GmlToken], position : Int32) -> GmlAttributes {
  let result = {
    next: position,
    id: None,
    label: None,
    source: None,
    target: None,
  }
  let mut index = position
  let mut depth = 0
  while index < tokens.length() {
    let token = tokens[index]
    if token.is_open() {
      depth = depth + 1
      index = index + 1
      continue
    }
    if token.is_close() {
      depth = depth - 1
      index = index + 1
      if depth <= 0 {
        break
      }
      continue
    }
    if token.is_word("id") && index + 1 < tokens.length() {
      result.id = tokens[index + 1].as_value()
      index = index + 2
      continue
    }
    if token.is_word("label") && index + 1 < tokens.length() {
      result.label = tokens[index + 1].as_value()
      index = index + 2
      continue
    }
    if token.is_word("source") && index + 1 < tokens.length() {
      result.source = tokens[index + 1].as_value()
      index = index + 2
      continue
    }
    if token.is_word("target") && index + 1 < tokens.length() {
      result.target = tokens[index + 1].as_value()
      index = index + 2
      continue
    }
    index = index + 1
  }
  result.next = index
  result
}

///|
/// A token of a GML string.
priv struct GmlToken {
  /// The word of the token, when it is a word.
  word : String
  /// The value of the token, when it is a quoted string or a number.
  value : String
  /// Whether the token is an opening bracket.
  open : Bool
  /// Whether the token is a closing bracket.
  close : Bool
}

///|
/// Whether the token is the given word.
fn GmlToken::is_word(self : GmlToken, word : String) -> Bool {
  self.word == word
}

///|
/// Whether the token is an opening bracket.
fn GmlToken::is_open(self : GmlToken) -> Bool {
  self.open
}

///|
/// Whether the token is a closing bracket.
fn GmlToken::is_close(self : GmlToken) -> Bool {
  self.close
}

///|
/// The value of the token, when it holds one.
fn GmlToken::as_value(self : GmlToken) -> String? {
  if self.value == "" {
    None
  } else {
    Some(self.value)
  }
}

///|
/// The tokens of a GML string.
///
/// A token is a word, a quoted string, a number, an opening bracket or a
/// closing bracket.
fn tokenize_gml(text : String) -> Array[GmlToken] {
  let tokens : Array[GmlToken] = []
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() {
    let character = chars[i]
    if character.is_whitespace() {
      i = i + 1
    } else if character == '[' {
      tokens.push(gml_token("", "", true, false))
      i = i + 1
    } else if character == ']' {
      tokens.push(gml_token("", "", false, true))
      i = i + 1
    } else if character == '"' {
      let start = i + 1
      let mut end = start
      while end < chars.length() && chars[end] != '"' {
        end = end + 1
      }
      tokens.push(
        gml_token("", gml_chars_to_string(chars, start, end), false, false),
      )
      i = if end < chars.length() { end + 1 } else { end }
    } else if character == '-' || character.is_ascii_digit() {
      let start = i
      i = i + 1
      while i < chars.length() &&
            (
              chars[i].is_ascii_digit() ||
              chars[i] == '.' ||
              chars[i] == 'e' ||
              chars[i] == 'E' ||
              chars[i] == '-' ||
              chars[i] == '+'
            ) {
        i = i + 1
      }
      tokens.push(
        gml_token("", gml_chars_to_string(chars, start, i), false, false),
      )
    } else {
      let start = i
      while i < chars.length() &&
            !chars[i].is_whitespace() &&
            chars[i] != '[' &&
            chars[i] != ']' &&
            chars[i] != '"' {
        i = i + 1
      }
      tokens.push(
        gml_token(gml_chars_to_string(chars, start, i), "", false, false),
      )
    }
  }
  tokens
}

///|
/// A token of a GML string.
fn gml_token(
  word : String,
  value : String,
  open : Bool,
  close : Bool,
) -> GmlToken {
  { word, value, open, close, }
}

///|
/// The text of a range of characters.
fn gml_chars_to_string(
  chars : Array[Char],
  start : Int32,
  end : Int32,
) -> String {
  let out : Array[Char] = []
  for i in start..