///|
fn append_roman_mapping_segment(
  mappings : Array[RomanEditMappingSegment],
  kind : RomanEditMappingKind,
  source_start : Int,
  source_end : Int,
  output_start : Int,
  output_end : Int,
) -> Unit {
  mappings.push({
    kind,
    source_span: { start: source_start, end: source_end },
    output_span: { start: output_start, end: output_end },
  })
}

///|
fn calculate_roman_edit_application(
  plan : RomanEditPlan,
  materialize_output : Bool,
) -> (RomanEditPreview, String) {
  let source_chars = plan.original.to_array()
  let applied_edits : Array[RomanAppliedEdit] = []
  let mapping_segments : Array[RomanEditMappingSegment] = []
  let mut source_cursor = 0
  let mut output_cursor = 0
  let mut output = ""
  for edit in plan.edits {
    if source_cursor < edit.span.start {
      let unchanged_length = edit.span.start - source_cursor
      append_roman_mapping_segment(
        mapping_segments,
        RomanUnchangedMapping,
        source_cursor,
        edit.span.start,
        output_cursor,
        output_cursor + unchanged_length,
      )
      if materialize_output {
        output = output +
          scan_substring(source_chars, source_cursor, edit.span.start)
      }
      output_cursor = output_cursor + unchanged_length
    }
    let replacement_length = edit.replacement.to_array().length()
    let output_span : SourceSpan = {
      start: output_cursor,
      end: output_cursor + replacement_length,
    }
    append_roman_mapping_segment(
      mapping_segments,
      RomanReplacedMapping(edit.id),
      edit.span.start,
      edit.span.end,
      output_span.start,
      output_span.end,
    )
    applied_edits.push({
      id: edit.id,
      source_span: edit.span,
      output_span,
      source_text: edit.expected_source,
      replacement_text: edit.replacement,
      numeric_value: edit.numeric_value,
      reason: edit.reason,
      source_diagnostic_code: edit.source_diagnostic_code,
    })
    if materialize_output {
      output = output + edit.replacement
    }
    source_cursor = edit.span.end
    output_cursor = output_span.end
  }
  if source_cursor < source_chars.length() {
    let unchanged_length = source_chars.length() - source_cursor
    append_roman_mapping_segment(
      mapping_segments,
      RomanUnchangedMapping,
      source_cursor,
      source_chars.length(),
      output_cursor,
      output_cursor + unchanged_length,
    )
    if materialize_output {
      output = output +
        scan_substring(source_chars, source_cursor, source_chars.length())
    }
  }
  (
    {
      original: plan.original,
      applied_edits,
      mapping_segments,
      statistics: plan.statistics,
    },
    output,
  )
}

///|
fn revalidate_roman_edit_plan(
  plan : RomanEditPlan,
) -> Result[RomanEditPlan, RomanEditError] {
  match validate_roman_edit_plan(plan.original, plan.edits, plan.config) {
    Err(error) => Err(error)
    Ok(validated) => Ok({ ..validated, diagnostics: plan.diagnostics })
  }
}

///|
/// Revalidate a plan before previewing exact output spans and mappings.
pub fn preview_roman_edit_plan(
  plan : RomanEditPlan,
) -> Result[RomanEditPreview, RomanEditError] {
  match revalidate_roman_edit_plan(plan) {
    Err(error) => Err(error)
    Ok(validated) => Ok(calculate_roman_edit_application(validated, false).0)
  }
}

///|
/// Apply every validated edit atomically when the complete source is current.
pub fn apply_roman_edit_plan(
  source : String,
  plan : RomanEditPlan,
) -> Result[RomanEditApplicationReport, RomanEditError] {
  if source != plan.original {
    return Err(RomanEditPlanSourceMismatch(plan.original, source))
  }
  let validated = match revalidate_roman_edit_plan(plan) {
    Err(error) => return Err(error)
    Ok(validated) => validated
  }
  let calculated = calculate_roman_edit_application(validated, true)
  let preview = calculated.0
  Ok({
    original: validated.original,
    output: calculated.1,
    applied_edits: preview.applied_edits,
    mapping_segments: preview.mapping_segments,
    statistics: preview.statistics,
  })
}