///|
fn is_blank_roman_edit_id(id : String) -> Bool {
  let chars = id.to_array()
  if chars.length() == 0 {
    return true
  }
  for character in chars {
    if !is_outer_whitespace(character) {
      return false
    }
  }
  true
}

///|
fn validate_roman_edit_ids(
  edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
  for index = 0; index < edits.length(); index = index + 1 {
    if is_blank_roman_edit_id(edits[index].id) {
      return Err(EmptyRomanEditId(index))
    }
    for previous = 0; previous < index; previous = previous + 1 {
      if edits[previous].id == edits[index].id {
        return Err(DuplicateRomanEditId(edits[index].id))
      }
    }
  }
  Ok(())
}

///|
fn validate_roman_edit_spans(
  edits : Array[RomanTextEdit],
  source_length : Int,
) -> Result[Unit, RomanEditError] {
  for edit in edits {
    if edit.span.start < 0 || edit.span.end < 0 {
      return Err(NegativeRomanEditSpan(edit.id, edit.span))
    }
    if edit.span.start > edit.span.end {
      return Err(ReversedRomanEditSpan(edit.id, edit.span))
    }
    if edit.span.end > source_length {
      return Err(RomanEditSpanOutOfBounds(edit.id, edit.span, source_length))
    }
  }
  Ok(())
}

///|
fn sort_roman_edits_by_source(
  edits : Array[RomanTextEdit],
) -> Array[RomanTextEdit] {
  let sorted : Array[RomanTextEdit] = []
  for edit in edits {
    sorted.push(edit)
    let mut index = sorted.length() - 1
    while index > 0 && sorted[index - 1].span.start > sorted[index].span.start {
      let previous = sorted[index - 1]
      sorted[index - 1] = sorted[index]
      sorted[index] = previous
      index = index - 1
    }
  }
  sorted
}

///|
fn validate_roman_edit_overlap(
  edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
  for index = 1; index < edits.length(); index = index + 1 {
    let previous = edits[index - 1]
    let current = edits[index]
    let same_start_with_replacement = current.span.start == previous.span.start &&
      (
        current.span.end > current.span.start ||
        previous.span.end > previous.span.start
      )
    if current.span.start < previous.span.end || same_start_with_replacement {
      return Err(OverlappingRomanEdits(previous.id, current.id))
    }
  }
  Ok(())
}

///|
fn validate_roman_edit_sources(
  source_chars : Array[Char],
  edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
  for edit in edits {
    let actual = scan_substring(source_chars, edit.span.start, edit.span.end)
    if actual != edit.expected_source {
      return Err(RomanEditSourceMismatch(edit.id, edit.expected_source, actual))
    }
  }
  Ok(())
}

///|
fn validate_roman_edit_no_ops(
  edits : Array[RomanTextEdit],
) -> Result[Unit, RomanEditError] {
  for edit in edits {
    if edit.replacement == edit.expected_source {
      return Err(NoOpRomanEdit(edit.id))
    }
  }
  Ok(())
}

///|
fn count_unchanged_roman_segments(
  source_length : Int,
  edits : Array[RomanTextEdit],
) -> Int {
  let mut count = 0
  let mut cursor = 0
  for edit in edits {
    if cursor < edit.span.start {
      count = count + 1
    }
    cursor = edit.span.end
  }
  if cursor < source_length {
    count = count + 1
  }
  count
}

///|
// Array and string lengths are Int-bounded. Even Int.max_value edits whose
// replacements each have Int.max_value characters remain below Int64.max_value.
fn accumulate_roman_edit_length(total : Int64, length : Int) -> Int64 {
  total + length.to_int64()
}

///|
fn project_roman_edit_length(
  source_length : Int,
  removed_length : Int64,
  inserted_length : Int64,
) -> Int64 {
  source_length.to_int64() - removed_length + inserted_length
}

///|
fn roman_max_representable_output_length() -> Int64 {
  2147483647L
}

///|
fn validate_roman_projected_output_representation(
  projected_length : Int64,
) -> Result[Unit, RomanEditError] {
  let limit = roman_max_representable_output_length()
  if projected_length > limit {
    Err(RomanProjectedOutputNotRepresentable(projected_length, limit))
  } else {
    Ok(())
  }
}

///|
fn roman_edit_plan_statistics(
  source_length : Int,
  edits : Array[RomanTextEdit],
) -> RomanEditPlanStatistics {
  let mut removed_length = 0L
  let mut inserted_length = 0L
  for edit in edits {
    removed_length = accumulate_roman_edit_length(
      removed_length,
      edit.span.end - edit.span.start,
    )
    inserted_length = accumulate_roman_edit_length(
      inserted_length,
      edit.replacement.to_array().length(),
    )
  }
  {
    edit_count: edits.length(),
    source_length: source_length.to_int64(),
    unchanged_segment_count: count_unchanged_roman_segments(
      source_length, edits,
    ),
    removed_length,
    inserted_length,
    projected_length: project_roman_edit_length(
      source_length, removed_length, inserted_length,
    ),
  }
}

///|
/// Validate and source-order a caller-supplied atomic text edit plan.
///
/// Validation order is configuration, IDs, spans, exact source evidence,
/// overlap, no-op replacements, representable output size, and configured size.
pub fn validate_roman_edit_plan(
  source : String,
  edits : Array[RomanTextEdit],
  config : RomanEditPlanConfig,
) -> Result[RomanEditPlan, RomanEditError] {
  if config.max_projected_length < 0 {
    return Err(InvalidRomanProjectedOutputLimit(config.max_projected_length))
  }
  match validate_roman_edit_ids(edits) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let source_chars = source.to_array()
  let source_length = source_chars.length()
  match validate_roman_edit_spans(edits, source_length) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let sorted = sort_roman_edits_by_source(edits)
  match validate_roman_edit_sources(source_chars, sorted) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_roman_edit_overlap(sorted) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_roman_edit_no_ops(sorted) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let statistics = roman_edit_plan_statistics(source_length, sorted)
  match
    validate_roman_projected_output_representation(statistics.projected_length) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if statistics.projected_length > config.max_projected_length {
    return Err(
      RomanProjectedOutputLimitExceeded(
        statistics.projected_length,
        config.max_projected_length,
      ),
    )
  }
  Ok({ original: source, config, edits: sorted, statistics, diagnostics: [] })
}