///|
/// Noise figure is dB, gamma_opt is a complex reflection coefficient relative
/// to Document::noise_reference_ohms, and resistance is always physical ohm.
pub(all) struct NoisePoint {
  frequency_hz : Double
  minimum_figure_db : Double
  gamma_opt : Complex
  resistance_ohms : Double
} derive(ToJson)

///|
pub extend NoisePoint with ToJson::{to_json}

///|
/// File metadata is separate from a Network so a transformation cannot silently
/// pretend that unchanged noise or mixed-mode metadata describe a new network.
pub struct Document {
  priv data : Network
  priv noise : Array[NoisePoint]
  priv noise_reference : Double
  priv mixed : Array[String]
  priv information : Array[String]
}

///|
fn port_number(s : String, n : Int) -> Int raise {
  if s.is_empty() || !s.iter().all(c => c >= '0' && c <= '9') {
    raise Failure("invalid port descriptor")
  }
  let p = @string.parse_int(s)
  if p < 1 || p > n {
    raise Failure("descriptor port outside 1..N")
  }
  p - 1
}

///|
fn validate_mixed(net : Network, modes : Array[String]) -> Unit raise {
  if modes.is_empty() {
    return
  }
  if modes.length() != net.n || net.kind == "H" || net.kind == "G" {
    raise Failure("mixed-mode descriptors require N entries and S/Y/Z")
  }
  let seen : Map[String, Bool] = Map([])
  let owners = Array::make(net.n, "")
  for token in modes {
    if token.length() < 2 || seen.contains(token) {
      raise Failure("duplicate/empty mixed-mode descriptor")
    }
    seen[token] = true
    let tail = token[1:].to_owned()
    if token[0] == 'S' {
      let p = port_number(tail, net.n)
      if owners[p] != "" {
        raise Failure("overlapping mixed-mode descriptors")
      }
      owners[p] = token
    } else if token[0] == 'D' || token[0] == 'C' {
      let pair = tail.split(",").map(x => x.to_owned()).to_array()
      if pair.length() != 2 {
        raise Failure("expected pair descriptor")
      }
      let p = port_number(pair[0], net.n)
      let q = port_number(pair[1], net.n)
      if p == q || net.reference[p] != net.reference[q] {
        raise Failure(
          "mode pair requires distinct ports with equal real references",
        )
      }
      let owner = "\{p},\{q}"
      if (owners[p] != "" && owners[p] != owner) ||
        (owners[q] != "" && owners[q] != owner) {
        raise Failure("overlapping mode pairs")
      }
      owners[p] = owner
      owners[q] = owner
    } else {
      raise Failure("unknown mixed-mode descriptor")
    }
  }
  for token in modes {
    if token[0] == 'D' || token[0] == 'C' {
      let other = (if token[0] == 'D' { "C" } else { "D" }) +
        token[1:].to_owned()
      if !seen.contains(other) {
        raise Failure("D/C descriptors must occur as matching ordered pairs")
      }
    }
  }
  if owners.any(x => x == "") {
    raise Failure("mixed-mode ports incomplete")
  }
}

///|
/// Construct a file document with owned metadata. Noise must be an increasing
/// two-port series whose first frequency is <= the final network frequency.
pub fn document(
  data : Network,
  noise? : Array[NoisePoint] = [],
  noise_reference_ohms? : Double = 50.0,
  mixed_mode_order? : Array[String] = [],
  information? : Array[String] = [],
) -> Document raise {
  if !finite(noise_reference_ohms) || noise_reference_ohms <= 0.0 {
    raise Failure("invalid noise reference")
  }
  if noise.length() > 100000 || (!noise.is_empty() && data.n != 2) {
    raise Failure("noise only supports two-port bounded data")
  }
  for i, p in noise {
    if !finite(p.frequency_hz) ||
      p.frequency_hz < 0.0 ||
      !finite(p.minimum_figure_db) ||
      !finite(p.resistance_ohms) ||
      p.resistance_ohms < 0.0 ||
      (i > 0 && p.frequency_hz <= noise[i - 1].frequency_hz) ||
      (
        i == 0 &&
        p.frequency_hz > data.frequencies[data.frequencies.length() - 1]
      ) {
      raise Failure("invalid noise frequency/order/resistance")
    }
    ignore(checked(p.gamma_opt))
    if !finite(p.gamma_opt.magnitude()) {
      raise Failure("noise reflection magnitude overflow")
    }
  }
  let modes = mixed_mode_order.map(x => x.to_upper())
  validate_mixed(data, modes)
  let mut total = 0
  for line in information {
    total += line.length()
    if total > 1000000 ||
      line.contains("\r") ||
      line.contains("\n") ||
      uncomment(line).to_upper() == "[END INFORMATION]" {
      raise Failure("invalid information line or size")
    }
    ignore(text_lines(line))
  }
  {
    data,
    noise: noise.copy(),
    noise_reference: noise_reference_ohms,
    mixed: modes,
    information: information.copy(),
  }
}

///|
/// Explicitly access raw matrices. For mixed mode, indices denote descriptors,
/// and references remain SINGLE-ENDED port references, not effective mode Z0.
pub fn Document::raw_network(self : Document) -> Network {
  self.data
}

///|
/// Extract an ordinary network only when no noise/mode metadata would be lost.
pub fn Document::network_only(self : Document) -> Network raise {
  if !self.noise.is_empty() || !self.mixed.is_empty() {
    raise Failure(
      "explicit metadata handling required before network operations",
    )
  }
  self.data
}

///|
pub fn Document::noise_data(self : Document) -> Array[NoisePoint] {
  self.noise.copy()
}

///|
pub fn Document::noise_reference_ohms(self : Document) -> Double {
  self.noise_reference
}

///|
pub fn Document::mixed_mode_order(self : Document) -> Array[String] {
  self.mixed.copy()
}

///|
pub fn Document::information_lines(self : Document) -> Array[String] {
  self.information.copy()
}

///|
fn noise_line(
  line : String,
  factor : Double,
  resistance_scale : Double,
) -> NoisePoint raise {
  let tokens = words(line)
  if tokens.length() != 5 {
    raise Failure("noise record requires five numbers on one line")
  }
  {
    frequency_hz: number(tokens[0]) * factor,
    minimum_figure_db: number(tokens[1]),
    gamma_opt: pair(number(tokens[2]), number(tokens[3]), "MA"),
    resistance_ohms: number(tokens[4]) * resistance_scale,
  }
}

///|
fn legacy_document(text : String, ports : Int) -> Document raise {
  if ports < 1 || ports > 32 {
    raise Failure("legacy ports must be 1..32")
  }
  let network_text = StringBuilder()
  let mut opt : TextOptions? = None
  let mut remainder = 0
  let mut last = -1.0
  let mut in_noise = false
  let noise = []
  let width = 1 + ports * ports * 2
  for raw in text_lines(text) {
    let line = uncomment(raw)
    if line.is_empty() {
      continue
    }
    if line.has_prefix("#") {
      if opt is None {
        opt = Some(read_options(line))
        network_text.write_string(line + "\n")
      }
      continue
    }
    guard opt is Some(o) else { raise Failure("missing option line") }
    let tokens = words(line)
    if !in_noise &&
      ports == 2 &&
      remainder == 0 &&
      last >= 0.0 &&
      number(tokens[0]) * o.factor <= last {
      in_noise = true
    }
    if in_noise {
      noise.push(noise_line(line, o.factor, o.reference[0]))
      if noise.length() > 100000 {
        raise Failure("noise point limit exceeded")
      }
    } else {
      for token in tokens {
        if remainder == 0 {
          last = number(token) * o.factor
        }
        remainder = (remainder + 1) % width
      }
      network_text.write_string(line + "\n")
    }
  }
  guard opt is Some(o) else { raise Failure("missing option line") }
  document(
    parse_legacy(network_text.to_string(), ports~),
    noise~,
    noise_reference_ohms=o.reference[0],
  )
}