///|
/// RFC 8866 line parser with bounded resource use and structured errors.
fn parse_origin_field(value : String, line : Int) -> Result[Origin, SdpError] {
  let parts = value.split(" ").to_array()
  if parts.length() != 6 {
    return Err(
      sdp_error(
        "origin", "invalid-field", line, 3, "o-line", "origin requires six space-separated fields",
      ),
    )
  }
  for part in parts {
    if part.length() == 0 {
      return Err(
        sdp_error(
          "origin", "invalid-spacing", line, 3, "o-line", "origin fields use one space separator",
        ),
      )
    }
  }
  if !valid_token(parts[0].to_owned()) ||
    !valid_token(parts[1].to_owned()) ||
    !valid_token(parts[2].to_owned()) ||
    !valid_token(parts[3].to_owned()) ||
    !valid_token(parts[4].to_owned()) ||
    parts[5].length() == 0 {
    return Err(
      sdp_error(
        "origin", "invalid-token", line, 3, "o-line", "origin contains an invalid token",
      ),
    )
  }
  Ok(
    origin(
      parts[0].to_owned(),
      parts[1].to_owned(),
      parts[2].to_owned(),
      parts[3].to_owned(),
      parts[4].to_owned(),
      parts[5].to_owned(),
    ),
  )
}

///|
fn parse_connection_field(
  value : String,
  line : Int,
) -> Result[Connection, SdpError] {
  let parts = value.split(" ").to_array()
  if parts.length() != 3 {
    return Err(
      sdp_error(
        "connection", "invalid-field", line, 3, "c-line", "connection requires network, address type and address",
      ),
    )
  }
  let address_parts = parts[2].split("/").to_array()
  if address_parts.length() == 0 || address_parts.length() > 3 {
    return Err(
      sdp_error(
        "connection", "invalid-address", line, 3, "c-line", "invalid connection-address suffix",
      ),
    )
  }
  let mut ttl : Int? = None
  let mut count : Int? = None
  if address_parts.length() >= 2 {
    match parse_non_negative_int(address_parts[1].to_owned()) {
      Some(value) => ttl = Some(value)
      None =>
        return Err(
          sdp_error(
            "connection", "invalid-ttl", line, 3, "c-line", "TTL must be a non-negative integer",
          ),
        )
    }
  }
  if address_parts.length() == 3 {
    match parse_non_negative_int(address_parts[2].to_owned()) {
      Some(value) => count = Some(value)
      None =>
        return Err(
          sdp_error(
            "connection", "invalid-count", line, 3, "c-line", "address count must be a non-negative integer",
          ),
        )
    }
  }
  Ok(
    connection(
      parts[0].to_owned(),
      parts[1].to_owned(),
      address_parts[0].to_owned(),
      ttl,
      count,
    ),
  )
}

///|
fn parse_bandwidth_field(
  value : String,
  line : Int,
) -> Result[Bandwidth, SdpError] {
  match value.split_once(":") {
    None =>
      Err(
        sdp_error(
          "bandwidth", "missing-colon", line, 3, "b-line", "bandwidth requires modifier:value",
        ),
      )
    Some(pair) => {
      let modifier = pair.0.to_owned()
      match parse_non_negative_int(pair.1.to_owned()) {
        None =>
          Err(
            sdp_error(
              "bandwidth", "invalid-value", line, 3, "b-line", "bandwidth value must be a non-negative integer",
            ),
          )
        Some(number) =>
          if !valid_token(modifier) {
            Err(
              sdp_error(
                "bandwidth", "invalid-modifier", line, 3, "b-line", "invalid bandwidth modifier",
              ),
            )
          } else {
            Ok(bandwidth(modifier, number))
          }
      }
    }
  }
}

///|
fn parse_attribute_field(
  value : String,
  line : Int,
) -> Result[Attribute, SdpError] {
  match value.split_once(":") {
    None =>
      if valid_attribute_name(value) {
        Ok(attribute(value, None, line))
      } else {
        Err(
          sdp_error(
            "attribute", "invalid-name", line, 3, "a-line", "attribute name is invalid",
          ),
        )
      }
    Some(pair) => {
      let name = pair.0.to_owned()
      if !valid_attribute_name(name) {
        Err(
          sdp_error(
            "attribute", "invalid-name", line, 3, "a-line", "attribute name is invalid",
          ),
        )
      } else {
        Ok(attribute(name, Some(pair.1.to_owned()), line))
      }
    }
  }
}

///|
fn parse_timing_field(value : String, line : Int) -> Result[Timing, SdpError] {
  let parts = value.split(" ").to_array()
  if parts.length() != 2 || parts[0].length() == 0 || parts[1].length() == 0 {
    Err(
      sdp_error(
        "timing", "invalid-field", line, 3, "t-line", "timing requires start and stop NTP values",
      ),
    )
  } else if parse_non_negative_int(parts[0].to_owned()) is None ||
    parse_non_negative_int(parts[1].to_owned()) is None {
    // RFC values can exceed this runtime's Int range. They remain strings,
    // but the lexical digit check is performed independently below.
    if all_digits(parts[0].to_owned()) && all_digits(parts[1].to_owned()) {
      Ok(timing(parts[0].to_owned(), parts[1].to_owned()))
    } else {
      Err(
        sdp_error(
          "timing", "invalid-number", line, 3, "t-line", "timing values contain non-digits",
        ),
      )
    }
  } else {
    Ok(timing(parts[0].to_owned(), parts[1].to_owned()))
  }
}

///|
fn all_digits(value : String) -> Bool {
  if value.length() == 0 {
    return false
  }
  for ch in value {
    if !is_ascii_digit(ch) {
      return false
    }
  }
  true
}

///|
fn parse_media_field(
  value : String,
  line : Int,
  limits : Limits,
) -> Result[MediaDescription, SdpError] {
  let parts = value.split(" ").to_array()
  if parts.length() < 4 {
    return Err(
      sdp_error(
        "media", "invalid-field", line, 3, "m-line", "media requires media, port, protocol and at least one format",
      ),
    )
  }
  for part in parts {
    if part.length() == 0 {
      return Err(
        sdp_error(
          "media", "invalid-spacing", line, 3, "m-line", "media fields use one space separator",
        ),
      )
    }
  }
  let port_parts = parts[1].split("/").to_array()
  if port_parts.length() == 0 || port_parts.length() > 2 {
    return Err(
      sdp_error(
        "media", "invalid-port", line, 3, "m-line", "media port syntax is invalid",
      ),
    )
  }
  let port = match parse_non_negative_int(port_parts[0].to_owned()) {
    None =>
      return Err(
        sdp_error(
          "media", "invalid-port", line, 3, "m-line", "media port must be an integer",
        ),
      )
    Some(value) => value
  }
  let mut port_count = 1
  if port_parts.length() == 2 {
    match parse_non_negative_int(port_parts[1].to_owned()) {
      None =>
        return Err(
          sdp_error(
            "media", "invalid-port-count", line, 3, "m-line", "port count must be an integer",
          ),
        )
      Some(value) => port_count = value
    }
  }
  let formats : Array[String] = Array::new()
  for index = 3; index < parts.length(); index = index + 1 {
    formats.push(parts[index].to_owned())
  }
  if formats.length() > limits.max_formats_per_media() {
    return Err(
      sdp_error(
        "limit", "too-many-formats", line, 3, "m-line", "media format count exceeds configured limit",
      ),
    )
  }
  Ok(
    media_description(
      parts[0].to_owned(),
      port,
      port_count,
      parts[2].to_owned(),
      formats,
    ),
  )
}

///|
/// Parses SDP using compatibility defaults while preserving every source line.
pub fn parse(input : String) -> Result[SessionDescription, SdpError] {
  match parse_with_options(input, compatible_options()) {
    Ok(report) => Ok(report.document())
    Err(error) => Err(error)
  }
}

///|
/// Parses SDP using the strict RFC line-ending and obsolete-key policy.
pub fn parse_strict(input : String) -> Result[SessionDescription, SdpError] {
  match parse_with_options(input, strict_options()) {
    Ok(report) => Ok(report.document())
    Err(error) => Err(error)
  }
}

///|
/// Parses SDP with caller-selected limits and compatibility policy.
pub fn parse_with_options(
  input : String,
  options : ParseOptions,
) -> Result[ParseReport, SdpError] {
  let active_limits = options.limits()
  if byte_length(input) > active_limits.max_input_bytes() {
    return Err(
      sdp_error(
        "limit", "input-too-large", 1, 1, "document", "input byte length exceeds configured limit",
      ),
    )
  }
  if contains_illegal_control(input) {
    return Err(
      sdp_error(
        "input", "control-character", 1, 1, "document", "input contains a forbidden control character",
      ),
    )
  }
  if input.length() == 0 {
    return Err(
      sdp_error("input", "empty", 1, 1, "document", "SDP document is empty"),
    )
  }
  let physical = input.split("\n").to_array()
  let logical_count = if input.has_suffix("\n") {
    physical.length() - 1
  } else {
    physical.length()
  }
  if logical_count > active_limits.max_lines() {
    return Err(
      sdp_error(
        "limit",
        "too-many-lines",
        active_limits.max_lines() + 1,
        1,
        "document",
        "line count exceeds configured limit",
      ),
    )
  }
  if !options.allow_lf() && !input.has_suffix("\r\n") {
    return Err(
      sdp_error(
        "input", "missing-crlf", logical_count, 1, "line-ending", "strict mode requires a final CRLF",
      ),
    )
  }
  let document = empty_session(input)
  let warnings : Array[SdpError] = Array::new()
  let mut current_media : MediaDescription? = None
  let mut saw_lf_only = false
  let mut attribute_count = 0
  for index = 0; index < logical_count; index = index + 1 {
    let line_number = index + 1
    let physical_line = physical[index].to_owned()
    if !physical_line.has_suffix("\r") {
      saw_lf_only = true
      if !options.allow_lf() {
        return Err(
          sdp_error(
            "input", "bare-lf", line_number, 1, "line-ending", "strict mode requires CRLF",
          ),
        )
      }
    }
    let raw = strip_one_cr(physical_line)
    if byte_length(raw) > active_limits.max_line_bytes() {
      return Err(
        sdp_error(
          "limit", "line-too-long", line_number, 1, "line", "line byte length exceeds configured limit",
        ),
      )
    }
    let chars = raw.to_array()
    if chars.length() < 2 || chars[1] != '=' {
      return Err(
        sdp_error(
          "line", "invalid-shape", line_number, 1, "line", "line must have the form type=value",
        ),
      )
    }
    if !is_ascii_alpha(chars[0]) || chars[0].to_int() > 127 {
      return Err(
        sdp_error(
          "line", "invalid-type", line_number, 1, "line", "field type must be one ASCII letter",
        ),
      )
    }
    let kind = String::from_array(chars[0:1])
    let value = String::from_array(chars[2:])
    let source = sdp_line(kind, value, line_number)
    document.source_lines.push(source)
    match chars[0] {
      'v' =>
        match parse_non_negative_int(value) {
          Some(number) => document.set_version(number)
          None =>
            return Err(
              sdp_error(
                "version", "invalid-number", line_number, 3, "v-line", "version must contain digits",
              ),
            )
        }
      'o' =>
        match parse_origin_field(value, line_number) {
          Ok(item) => document.set_origin(item)
          Err(error) => return Err(error)
        }
      's' => document.set_session_name(value)
      'i' =>
        match current_media {
          None => document.set_information(value)
          Some(media) => media.set_information(value)
        }
      'u' => document.set_uri(value)
      'e' => document.emails.push(value)
      'p' => document.phones.push(value)
      'c' =>
        match parse_connection_field(value, line_number) {
          Err(error) => return Err(error)
          Ok(item) =>
            match current_media {
              None => document.set_connection(item)
              Some(media) => media.connections.push(item)
            }
        }
      'b' =>
        match parse_bandwidth_field(value, line_number) {
          Err(error) => return Err(error)
          Ok(item) =>
            match current_media {
              None => document.bandwidths.push(item)
              Some(media) => media.bandwidths.push(item)
            }
        }
      't' =>
        match current_media {
          Some(_) =>
            return Err(
              sdp_error(
                "order", "session-field-after-media", line_number, 1, "t-line", "timing is only valid at session level",
              ),
            )
          None =>
            match parse_timing_field(value, line_number) {
              Err(error) => return Err(error)
              Ok(item) => {
                if document.timings.length() >= active_limits.max_timings() {
                  return Err(
                    sdp_error(
                      "limit", "too-many-timings", line_number, 1, "t-line", "timing count exceeds configured limit",
                    ),
                  )
                }
                document.timings.push(item)
              }
            }
        }
      'r' => {
        if current_media is Some(_) || document.timings.length() == 0 {
          return Err(
            sdp_error(
              "timing", "orphan-repeat", line_number, 1, "r-line", "repeat must follow a time field",
            ),
          )
        }
        let timing_item = document.timings[document.timings.length() - 1]
        if timing_item.repeats().length() >=
          active_limits.max_repeats_per_timing() {
          return Err(
            sdp_error(
              "limit", "too-many-repeats", line_number, 1, "r-line", "repeat count exceeds configured limit",
            ),
          )
        }
        timing_item.repeats().push(value)
      }
      'z' => document.set_timezone_adjustments(value)
      'k' => {
        if !options.allow_obsolete_key() {
          return Err(
            sdp_error(
              "key", "obsolete-field", line_number, 1, "k-line", "RFC 8866 marks k= obsolete",
            ),
          )
        }
        match current_media {
          None => document.set_encryption_key(value)
          Some(_) => ()
        }
        warnings.push(
          sdp_error(
            "key", "obsolete-field", line_number, 1, "k-line", "obsolete k= field preserved",
          ),
        )
      }
      'a' => {
        attribute_count = attribute_count + 1
        if attribute_count > active_limits.max_attributes() {
          return Err(
            sdp_error(
              "limit", "too-many-attributes", line_number, 1, "a-line", "attribute count exceeds configured limit",
            ),
          )
        }
        match parse_attribute_field(value, line_number) {
          Err(error) => return Err(error)
          Ok(item) =>
            match current_media {
              None => document.attributes.push(item)
              Some(media) => media.attributes.push(item)
            }
        }
      }
      'm' => {
        match current_media {
          Some(media) => document.media_descriptions.push(media)
          None => ()
        }
        if document.media_descriptions.length() >= active_limits.max_media() {
          return Err(
            sdp_error(
              "limit", "too-many-media", line_number, 1, "m-line", "media count exceeds configured limit",
            ),
          )
        }
        match parse_media_field(value, line_number, active_limits) {
          Err(error) => return Err(error)
          Ok(media) => {
            media.source_lines.push(source)
            current_media = Some(media)
          }
        }
      }
      _ =>
        return Err(
          sdp_error(
            "line", "unknown-type", line_number, 1, "line", "unknown SDP field type",
          ),
        )
    }
    match current_media {
      Some(media) => if chars[0] != 'm' { media.source_lines.push(source) }
      None => ()
    }
  }
  match current_media {
    Some(media) => document.media_descriptions.push(media)
    None => ()
  }
  if saw_lf_only {
    warnings.push(
      sdp_error(
        "input", "bare-lf", 1, 1, "line-ending", "LF-only input accepted in compatibility mode",
      ),
    )
  }
  if document.origin() is None {
    return Err(
      sdp_error(
        "required", "missing-origin", 1, 1, "document", "required o= field is missing",
      ),
    )
  }
  if document.session_name().length() == 0 {
    return Err(
      sdp_error(
        "required", "empty-session-name", 1, 1, "s-line", "session name must not be empty",
      ),
    )
  }
  if document.timings().length() == 0 {
    return Err(
      sdp_error(
        "required", "missing-timing", 1, 1, "document", "at least one t= field is required",
      ),
    )
  }
  Ok(parse_report(document, warnings))
}