///|
priv struct TextOptions {
  factor : Double
  kind : String
  format : String
  reference : Array[Double]
}

///|
// Validate before UTF-16 slicing: Touchstone is ASCII, not arbitrary Unicode.
// The cap bounds token allocation, not merely the final network array.
fn text_lines(text : String) -> Array[String] raise {
  if text.length() > 64000000 {
    raise Failure("text exceeds 64 million characters")
  }
  let normalized = StringBuilder()
  let mut previous_cr = false
  let mut lines = 1
  let mut width = 0
  for c in text {
    if c == '\r' || (c == '\n' && !previous_cr) {
      lines += 1
      width = 0
      if lines > 1000000 {
        raise Failure("text exceeds one million lines")
      }
      normalized.write_char('\n')
    } else if c == '\n' {
      // CRLF is one physical line, including inside preserved information.
      ()
    } else if c == '\t' || (c >= ' ' && c <= '~') {
      width += 1
      if width > 262144 {
        raise Failure("text line exceeds 262144 characters")
      }
      normalized.write_char(c)
    } else {
      raise Failure("Touchstone requires printable ASCII, tab and line endings")
    }
    previous_cr = c == '\r'
  }
  normalized.to_string().split("\n").map(x => x.to_owned()).to_array()
}

///|
fn uncomment(line : String) -> String {
  let end = line.find("!").unwrap_or(line.length())
  line[:end].trim().to_owned()
}

///|
fn words(line : String) -> Array[String] {
  let result = []
  let mut start = 0
  for i in 0..<=line.length() {
    if i == line.length() || line[i] == ' ' || line[i] == '\t' {
      if start < i {
        result.push(line[start:i].to_owned())
      }
      start = i + 1
    }
  }
  result
}

///|
fn number(token : String) -> Double raise {
  if token.contains("_") {
    raise Failure("underscores are not Touchstone numbers")
  }
  let x = @string.parse_double(token)
  if !finite(x) {
    raise Failure("non-finite numeric field")
  }
  x
}

///|
fn unit_factor(unit : String) -> Double raise {
  match unit.to_upper() {
    "HZ" => 1.0
    "KHZ" => 1000.0
    "MHZ" => 1000000.0
    "GHZ" => 1000000000.0
    _ => raise Failure("unsupported frequency unit")
  }
}

///|
fn read_options(line : String) -> TextOptions raise {
  let tokens = words(line[1:].to_owned())
  let mut unit = "GHZ"
  let mut kind = "S"
  let mut format = "MA"
  let refs = []
  let seen : Map[String, Bool] = Map([])
  let mut i = 0
  while i < tokens.length() {
    let token = tokens[i].to_upper()
    let key = match token {
      "HZ" | "KHZ" | "MHZ" | "GHZ" => {
        unit = token
        "unit"
      }
      "S" | "Y" | "Z" | "H" | "G" => {
        kind = token
        "kind"
      }
      "RI" | "MA" | "DB" => {
        format = token
        "format"
      }
      "R" => {
        i += 1
        while i < tokens.length() {
          let next = tokens[i].to_upper()
          if next == "HZ" ||
            next == "KHZ" ||
            next == "MHZ" ||
            next == "GHZ" ||
            next == "S" ||
            next == "Y" ||
            next == "Z" ||
            next == "H" ||
            next == "G" ||
            next == "RI" ||
            next == "MA" ||
            next == "DB" ||
            next == "R" {
            break
          }
          let r = number(tokens[i])
          if r <= 0.0 {
            raise Failure("reference must be positive")
          }
          refs.push(r)
          if refs.length() > 32 {
            raise Failure("too many references")
          }
          i += 1
        }
        if refs.is_empty() {
          raise Failure("R requires a reference resistance")
        }
        if refs.length() > 1 && i != tokens.length() {
          raise Failure("per-port R must end option line")
        }
        i -= 1
        "reference"
      }
      _ => raise Failure("unknown option: " + token)
    }
    if seen.contains(key) {
      raise Failure("duplicate option category: " + key)
    }
    seen[key] = true
    i += 1
  }
  if refs.is_empty() {
    refs.push(50.0)
  }
  { factor: unit_factor(unit), kind, format, reference: refs, }
}

///|
fn references(refs : Array[Double], n : Int) -> Array[Double] raise {
  if refs.length() == 1 {
    Array::make(n, refs[0])
  } else if refs.length() == n {
    refs.copy()
  } else {
    raise Failure("reference count must be one or match ports")
  }
}

///|
fn pair(a : Double, b : Double, format : String) -> Complex raise {
  if format == "RI" {
    return checked(complex(a, im=b))
  }
  let magnitude = if format == "DB" { @math.pow(10.0, a / 20.0) } else { a }
  if magnitude < 0.0 || !finite(magnitude) {
    raise Failure("invalid magnitude")
  }
  let angle = b / 180.0 * @math.PI
  checked(
    complex(magnitude * @math.cos(angle), im=magnitude * @math.sin(angle)),
  )
}

///|
// IBIS 2.1 p8: legacy Y/Z/H/G are normalized. Scale voltages by sqrt(R)
// and currents by 1/sqrt(R); H maps (I1,V2) -> (V1,I2), G reverses that.
fn legacy_factor(
  kind : String,
  row : Int,
  col : Int,
  refs : Array[Double],
) -> Double {
  let dr = refs[row].sqrt()
  let dc = refs[col].sqrt()
  match kind {
    "Z" => dr * dc
    "Y" => 1.0 / dr / dc
    "H" =>
      (if row == 0 { dr } else { 1.0 / dr }) *
      (if col == 0 { dc } else { 1.0 / dc })
    "G" =>
      (if row == 0 { 1.0 / dr } else { dr }) *
      (if col == 0 { 1.0 / dc } else { dc })
    _ => 1.0
  }
}

///|
fn write_pair(
  out : StringBuilder,
  value : Complex,
  format : String,
) -> Unit raise {
  ignore(checked(value))
  if format == "RI" {
    out.write_string(" \{value.re} \{value.im}")
  } else {
    let mag = value.magnitude()
    // -Inf is not portable Touchstone numeric syntax. Use RI/MA for exact zero.
    if format == "DB" && mag == 0.0 {
      raise Failure(
        "zero magnitude cannot be encoded as finite DB; use RI or MA",
      )
    }
    let a = if format == "DB" { 20.0 * @math.log10(mag) } else { mag }
    let b = value.phase() * 180.0 / @math.PI
    if !finite(a) || !finite(b) {
      raise Failure("non-finite formatted value")
    }
    out.write_string(" \{a} \{b}")
  }
}

///|
// Keep serialized output inside the same file-size contract as the reader.
fn bounded_output(out : StringBuilder) -> String raise {
  let result = out.to_string()
  if result.length() > 64000000 {
    raise Failure("serialized text exceeds 64 million characters")
  }
  result
}