///|
fn integer(text : String) -> Int raise {
  if text.is_empty() || !text.iter().all(c => c >= '0' && c <= '9') {
    raise Failure("positive decimal integer expected")
  }
  let n = @string.parse_int(text)
  if n <= 0 {
    raise Failure("positive integer expected")
  }
  n
}

///|
fn keyword(line : String) -> (String, String) raise {
  guard line.find("]") is Some(end) else {
    raise Failure("unterminated keyword")
  }
  if end + 1 < line.length() && line[end + 1] != ' ' && line[end + 1] != '\t' {
    raise Failure("keyword argument needs whitespace")
  }
  (line[1:end].to_owned().to_upper(), line[end + 1:].trim().to_owned())
}

///|
/// Read a 1.x or 2.0/2.1 document. Legacy requires explicit ports. For 2.x a
/// supplied ports value is a consistency check, never an override of the file.
pub fn parse(text : String, ports? : Int) -> Document raise {
  let lines = text_lines(text)
  let mut first = ""
  for raw in lines {
    let line = uncomment(raw)
    if !line.is_empty() {
      first = line
      break
    }
  }
  if !first.to_upper().has_prefix("[VERSION]") {
    guard ports is Some(n) else {
      raise Failure("legacy file needs port count")
    }
    return legacy_document(text, n)
  }
  let headers : Map[String, String] = Map([])
  let numbers : Array[Double] = []
  let noise_text = []
  let information = []
  let mut mode = "header"
  let mut continuation = ""
  let mut opt : TextOptions? = None
  let mut info_size = 0
  for raw in lines {
    let line = uncomment(raw)
    if mode == "information" {
      if line.to_upper() == "[END INFORMATION]" {
        mode = "header"
      } else {
        info_size += raw.length()
        if info_size > 1000000 {
          raise Failure("information exceeds one million characters")
        }
        information.push(raw)
      }
      continue
    }
    if line.is_empty() {
      continue
    }
    if mode == "end" {
      raise Failure("content after End")
    }
    if line.has_prefix("#") {
      if opt is None {
        if !headers.contains("VERSION") || headers.contains("NUMBER OF PORTS") {
          raise Failure("option line out of order")
        }
        let o = read_options(line)
        if o.reference.length() != 1 {
          raise Failure("2.x per-port references require Reference keyword")
        }
        opt = Some(o)
      }
      continue
    }
    if line.has_prefix("[") {
      if raw[0] != '[' {
        raise Failure("keyword must start in column one")
      }
      let (key, value) = keyword(line)
      continuation = ""
      if headers.contains(key) {
        raise Failure("duplicate keyword: " + key)
      }
      match key {
        "VERSION" =>
          if !headers.is_empty() || (value != "2.0" && value != "2.1") {
            raise Failure("unsupported or misplaced Version")
          }
        "NUMBER OF PORTS" => {
          if opt is None || mode != "header" {
            raise Failure("Number of Ports requires option line")
          }
          let n = integer(value)
          if n > 32 {
            raise Failure("at most 32 ports")
          }
        }
        "NETWORK DATA" => {
          if mode != "header" ||
            !headers.contains("NUMBER OF FREQUENCIES") ||
            !value.is_empty() {
            raise Failure("invalid Network Data boundary")
          }
          mode = "data"
        }
        "NOISE DATA" => {
          if mode != "data" ||
            !headers.contains("NUMBER OF NOISE FREQUENCIES") ||
            !value.is_empty() {
            raise Failure("invalid Noise Data boundary")
          }
          mode = "noise"
        }
        "END" => {
          if (mode != "data" && mode != "noise") || !value.is_empty() {
            raise Failure("invalid End boundary")
          }
          mode = "end"
        }
        "NUMBER OF FREQUENCIES"
        | "NUMBER OF NOISE FREQUENCIES"
        | "REFERENCE"
        | "MATRIX FORMAT"
        | "TWO-PORT DATA ORDER"
        | "MIXED-MODE ORDER"
        | "BEGIN INFORMATION" => {
          if mode != "header" || !headers.contains("NUMBER OF PORTS") {
            raise Failure("header keyword out of order")
          }
          if key == "REFERENCE" || key == "MIXED-MODE ORDER" {
            continuation = key
          }
          if key == "BEGIN INFORMATION" {
            if !value.is_empty() {
              raise Failure("Begin Information takes no argument")
            }
            mode = "information"
          }
        }
        _ => raise Failure("unsupported keyword: " + key)
      }
      if (key == "REFERENCE" || key == "MIXED-MODE ORDER") &&
        value.length() > 4096 {
        raise Failure("reference/mode header exceeds 4096 characters")
      }
      headers[key] = value
      continue
    }
    match mode {
      "header" => {
        if continuation == "" {
          raise Failure("unexpected numeric header content")
        }
        if headers[continuation].length() + line.length() + 1 > 4096 {
          raise Failure("reference/mode header exceeds 4096 characters")
        }
        headers[continuation] = headers[continuation] + " " + line
      }
      "data" => {
        for token in words(line) {
          numbers.push(number(token))
        }
        if numbers.length() > 4100000 {
          raise Failure("network data exceeds value limit")
        }
      }
      "noise" => {
        noise_text.push(line)
        if noise_text.length() > 100000 {
          raise Failure("noise limit exceeded")
        }
      }
      _ => raise Failure("unexpected data")
    }
  }
  if mode != "end" {
    raise Failure("missing End or unterminated information")
  }
  guard opt is Some(o) else { raise Failure("missing option line") }
  guard headers.get("NUMBER OF PORTS") is Some(ntext) else {
    raise Failure("missing Number of Ports")
  }
  let n = integer(ntext)
  if ports is Some(p) && p != n {
    raise Failure("filename/caller port count conflicts with header")
  }
  let count = integer(headers["NUMBER OF FREQUENCIES"])
  if count > 100000 || count > 2000000 / (n * n) {
    raise Failure("point count exceeds resource limit")
  }
  let refs = if headers.get("REFERENCE") is Some(r) {
    let r = words(r).map(number)
    if r.length() != n {
      raise Failure("Reference needs exactly N values")
    }
    r
  } else {
    references(o.reference, n)
  }
  let format = headers.get("MATRIX FORMAT").unwrap_or("Full").to_upper()
  if format != "FULL" && format != "UPPER" && format != "LOWER" {
    raise Failure("unsupported matrix layout")
  }
  let order = headers.get("TWO-PORT DATA ORDER").unwrap_or("").to_upper()
  if (n == 2 && order != "12_21" && order != "21_12") || (n != 2 && order != "") {
    raise Failure("invalid/missing Two-Port Data Order")
  }
  let indices = []
  for row in 0..= row) ||
        (format == "LOWER" && col <= row) {
        indices.push(row * n + col)
      }
    }
  }
  if n == 2 && format == "FULL" && order == "21_12" {
    indices[1] = 2
    indices[2] = 1
  }
  let width = 1 + 2 * indices.length()
  if numbers.length() != count * width {
    raise Failure("Number of Frequencies does not match network data length")
  }
  let freq = []
  let matrices = []
  for p in 0.. noise_line(line, o.factor, 1.0))
  if headers.get("NUMBER OF NOISE FREQUENCIES") is Some(c) {
    if integer(c) != noise.length() || !headers.contains("NOISE DATA") {
      raise Failure("noise count/boundary mismatch")
    }
  } else if !noise.is_empty() {
    raise Failure("unexpected noise")
  }
  let mixed = words(headers.get("MIXED-MODE ORDER").unwrap_or(""))
  if headers.contains("MIXED-MODE ORDER") && mixed.is_empty() {
    raise Failure("empty mixed-mode order")
  }
  document(
    data,
    noise~,
    noise_reference_ohms=o.reference[0],
    mixed_mode_order=mixed,
    information~,
  )
}

///|
/// Canonical 2.1 output in Hz/RI/full/12_21 form; preserves noise, mode order,
/// information and references. Comments and original whitespace are not kept.
pub fn Document::write(self : Document) -> String raise {
  let n = self.data.n
  let out = StringBuilder()
  out.write_string(
    "[Version] 2.1\n# Hz \{self.data.kind} RI R \{self.noise_reference}\n[Number of Ports] \{n}\n",
  )
  if n == 2 {
    out.write_string("[Two-Port Data Order] 12_21\n")
  }
  out.write_string(
    "[Number of Frequencies] \{self.data.frequencies.length()}\n[Reference]",
  )
  for r in self.data.reference {
    out.write_string(" \{r}")
  }
  out.write_string("\n[Matrix Format] Full\n")
  if !self.noise.is_empty() {
    out.write_string("[Number of Noise Frequencies] \{self.noise.length()}\n")
  }
  if !self.mixed.is_empty() {
    out.write_string("[Mixed-Mode Order] " + self.mixed.join(" ") + "\n")
  }
  if !self.information.is_empty() {
    out.write_string(
      "[Begin Information]\n" +
      self.information.join("\n") +
      "\n[End Information]\n",
    )
  }
  out.write_string("[Network Data]\n")
  for p, f in self.data.frequencies {
    out.write_string("\{f}")
    for k, z in self.data.values[p] {
      out.write_string(" \{z.re} \{z.im}")
      if n > 2 && ((k % n + 1) % 4 == 0 || k % n == n - 1) && k + 1 < n * n {
        out.write_string("\n ")
      }
    }
    out.write_string("\n")
  }
  if !self.noise.is_empty() {
    out.write_string("[Noise Data]\n")
    for p in self.noise {
      let magnitude = p.gamma_opt.magnitude()
      let phase = p.gamma_opt.phase() * 180.0 / @math.PI
      out.write_string(
        "\{p.frequency_hz} \{p.minimum_figure_db} \{magnitude} \{phase} \{p.resistance_ohms}\n",
      )
    }
  }
  out.write_string("[End]\n")
  bounded_output(out)
}