///|
pub fn mapping_segment(finding : Finding, offset : OffsetMap) -> MappingSegment {
  {
    original: { start: offset.original_start, end: offset.original_end },
    redacted: { start: offset.replacement_start, end: offset.replacement_end },
    finding_id: finding.id,
    kind: finding.kind,
  }
}

///|
pub fn build_mapping_index(
  input_length : Int,
  output_length : Int,
  findings : Array[Finding],
  offsets : Array[OffsetMap],
) -> MappingIndex {
  let segments = []
  for offset in offsets {
    for finding in findings {
      if finding.id == offset.finding_id {
        segments.push(mapping_segment(finding, offset))
        break
      }
    }
  }
  segments.sort_by(fn(left, right) {
    left.original.start - right.original.start
  })
  { original_length: input_length, redacted_length: output_length, segments }
}

///|
pub fn mapping_from_result(result : DeidResult) -> MappingIndex {
  build_mapping_index(
    result.audit.input_length,
    result.audit.output_length,
    result.findings,
    result.offsets,
  )
}

///|
pub fn MappingIndex::is_monotonic(self : MappingIndex) -> Bool {
  for i in 1.. current.original.start ||
      before.redacted.end > current.redacted.start {
      return false
    }
  }
  true
}

///|
fn map_inside(source : Span, target : Span, position : Int) -> Int {
  if position <= source.start {
    target.start
  } else if position >= source.end {
    target.end
  } else {
    let source_length = source.length()
    let target_length = target.length()
    let relative = position - source.start
    target.start +
    (if source_length == 0 {
      0
    } else {
      relative * target_length / source_length
    })
  }
}

///|
pub fn MappingIndex::original_to_redacted(
  self : MappingIndex,
  position : Int,
) -> Int {
  let bounded = if position < 0 {
    0
  } else if position > self.original_length {
    self.original_length
  } else {
    position
  }
  let mut previous_original = 0
  let mut previous_redacted = 0
  for segment in self.segments {
    if bounded < segment.original.start {
      return previous_redacted + bounded - previous_original
    }
    if bounded <= segment.original.end {
      return map_inside(segment.original, segment.redacted, bounded)
    }
    previous_original = segment.original.end
    previous_redacted = segment.redacted.end
  }
  previous_redacted + bounded - previous_original
}

///|
pub fn MappingIndex::redacted_to_original(
  self : MappingIndex,
  position : Int,
) -> Int {
  let bounded = if position < 0 {
    0
  } else if position > self.redacted_length {
    self.redacted_length
  } else {
    position
  }
  let mut previous_original = 0
  let mut previous_redacted = 0
  for segment in self.segments {
    if bounded < segment.redacted.start {
      return previous_original + bounded - previous_redacted
    }
    if bounded <= segment.redacted.end {
      return map_inside(segment.redacted, segment.original, bounded)
    }
    previous_original = segment.original.end
    previous_redacted = segment.redacted.end
  }
  previous_original + bounded - previous_redacted
}

///|
pub fn MappingIndex::original_range_to_redacted(
  self : MappingIndex,
  span : Span,
) -> Span {
  {
    start: self.original_to_redacted(span.start),
    end: self.original_to_redacted(span.end),
  }
}

///|
pub fn MappingIndex::redacted_range_to_original(
  self : MappingIndex,
  span : Span,
) -> Span {
  {
    start: self.redacted_to_original(span.start),
    end: self.redacted_to_original(span.end),
  }
}

///|
pub fn MappingIndex::original_line_column(
  self : MappingIndex,
  original : String,
  position : Int,
) -> LineColumn {
  let mapped = self.original_to_redacted(position)
  ignore(mapped)
  line_column(original, position)
}

///|
pub fn MappingIndex::redacted_line_column(
  self : MappingIndex,
  redacted : String,
  position : Int,
) -> LineColumn {
  let mapped = self.redacted_to_original(position)
  ignore(mapped)
  line_column(redacted, position)
}

///|
pub fn MappingIndex::segment_for_original(
  self : MappingIndex,
  position : Int,
) -> MappingSegment? {
  for item in self.segments {
    if item.original.contains(position) {
      return Some(item)
    }
  }
  None
}

///|
pub fn MappingIndex::segment_for_redacted(
  self : MappingIndex,
  position : Int,
) -> MappingSegment? {
  for item in self.segments {
    if item.redacted.contains(position) {
      return Some(item)
    }
  }
  None
}

///|
pub fn MappingIndex::changed_original_spans(self : MappingIndex) -> Array[Span] {
  self.segments.map(fn(item) { item.original })
}

///|
pub fn MappingIndex::changed_redacted_spans(self : MappingIndex) -> Array[Span] {
  self.segments.map(fn(item) { item.redacted })
}

///|
pub fn MappingIndex::describe(self : MappingIndex) -> String {
  let lines = [
    "original_length=\{self.original_length}",
    "redacted_length=\{self.redacted_length}",
    "segments=\{self.segments.length()}",
    "monotonic=\{self.is_monotonic()}",
  ]
  for segment in self.segments {
    lines.push(
      "- \{segment.finding_id} \{segment.original.start}..\{segment.original.end} -> \{segment.redacted.start}..\{segment.redacted.end}",
    )
  }
  lines.join("\n")
}

///|
pub fn offsets_are_monotonic(offsets : Array[OffsetMap]) -> Bool {
  for i in 1.. offsets[i].original_start ||
      offsets[i - 1].replacement_end > offsets[i].replacement_start {
      return false
    }
  }
  true
}

///|
pub fn offset_delta(offset : OffsetMap) -> Int {
  offset.replacement_end -
  offset.replacement_start -
  (offset.original_end - offset.original_start)
}

///|
pub fn cumulative_offset_delta(offsets : Array[OffsetMap]) -> Array[Int] {
  let result = [0]
  let mut current = 0
  for offset in offsets {
    current += offset_delta(offset)
    result.push(current)
  }
  result
}

///|
pub fn map_position_through_offsets(
  offsets : Array[OffsetMap],
  position : Int,
) -> Int {
  let mut delta = 0
  for offset in offsets {
    if position < offset.original_start {
      break
    } else if position <= offset.original_end {
      return offset.replacement_start
    } else {
      delta += offset_delta(offset)
    }
  }
  position + delta
}

///|
pub fn inverse_position_through_offsets(
  offsets : Array[OffsetMap],
  position : Int,
) -> Int {
  let mut delta = 0
  for offset in offsets {
    if position < offset.replacement_start {
      break
    } else if position <= offset.replacement_end {
      return offset.original_start
    } else {
      delta += offset_delta(offset)
    }
  }
  position - delta
}