///|
pub(all) struct SourcePosition {
  line : Int
  column : Int
  offset : Int
} derive(Debug, Eq)

///|
pub(all) struct SourceRange {
  start : SourcePosition
  end : SourcePosition
} derive(Debug, Eq)

///|
pub(all) struct LineSlice {
  line : Int
  start_offset : Int
  end_offset : Int
  text : String
} derive(Debug, Eq)

///|
pub(all) struct LineIndex {
  source : String
  lines : Array[LineSlice]
  chars : Int
} derive(Debug, Eq)

///|
pub(all) struct LocatedFinding {
  finding : Finding
  position : SourcePosition
  line_text : String
  caret : String
} derive(Debug, Eq)

///|
fn clamp_position_value(value : Int, low : Int, high : Int) -> Int {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn count_chars_for_index(input : String) -> Int {
  input.iter().count()
}

///|
fn make_line_slice(
  line : Int,
  start_offset : Int,
  end_offset : Int,
  chars : Array[Char],
) -> LineSlice {
  { line, start_offset, end_offset, text: String::from_array(chars) }
}

///|
pub fn build_line_index(source : String) -> LineIndex {
  let lines : Array[LineSlice] = []
  let current : Array[Char] = []
  let mut line = 1
  let mut line_start = 0
  let mut offset = 0
  for c in source {
    if c == '\n' {
      lines.push(make_line_slice(line, line_start, offset, current))
      current.clear()
      offset += 1
      line += 1
      line_start = offset
    } else {
      current.push(c)
      offset += 1
    }
  }
  lines.push(make_line_slice(line, line_start, offset, current))
  { source, lines, chars: offset }
}

///|
fn fallback_empty_line() -> LineSlice {
  { line: 1, start_offset: 0, end_offset: 0, text: "" }
}

///|
pub fn LineIndex::line_count(self : LineIndex) -> Int {
  self.lines.length()
}

///|
pub fn LineIndex::is_empty(self : LineIndex) -> Bool {
  self.chars == 0
}

///|
pub fn LineIndex::line_at(self : LineIndex, line : Int) -> LineSlice? {
  if line <= 0 {
    return None
  }
  let index = line - 1
  if index >= self.lines.length() {
    None
  } else {
    Some(self.lines[index])
  }
}

///|
pub fn LineIndex::line_for_offset(self : LineIndex, offset : Int) -> LineSlice {
  if self.lines.length() == 0 {
    return fallback_empty_line()
  }
  let target = clamp_position_value(offset, 0, self.chars)
  for slice in self.lines {
    if target >= slice.start_offset && target <= slice.end_offset {
      return slice
    }
  }
  self.lines[self.lines.length() - 1]
}

///|
pub fn LineIndex::position_at(self : LineIndex, offset : Int) -> SourcePosition {
  let target = clamp_position_value(offset, 0, self.chars)
  let slice = self.line_for_offset(target)
  let raw_column = target - slice.start_offset + 1
  let max_column = count_chars_for_index(slice.text) + 1
  {
    line: slice.line,
    column: clamp_position_value(raw_column, 1, max_column),
    offset: target,
  }
}

///|
pub fn SourcePosition::format(self : SourcePosition) -> String {
  "line \{self.line}, column \{self.column}"
}

///|
pub fn SourcePosition::compact(self : SourcePosition) -> String {
  "\{self.line}:\{self.column}"
}

///|
pub fn format_position(position : SourcePosition) -> String {
  position.format()
}

///|
pub fn compact_position(position : SourcePosition) -> String {
  position.compact()
}

///|
fn repeat_index_char(c : Char, count : Int) -> String {
  let out = StringBuilder::new()
  let mut i = 0
  while i < count {
    out.write_char(c)
    i += 1
  }
  out.to_string()
}

///|
pub fn caret_for_column(column : Int) -> String {
  repeat_index_char(' ', clamp_position_value(column - 1, 0, column)) + "^"
}

///|
pub fn LineIndex::caret_at(self : LineIndex, offset : Int) -> String {
  caret_for_column(self.position_at(offset).column)
}

///|
fn line_number_width(max_line : Int) -> Int {
  let mut value = max_line
  let mut width = 1
  while value >= 10 {
    value = value / 10
    width += 1
  }
  width
}

///|
fn pad_left_number(value : Int, width : Int) -> String {
  let text = value.to_string()
  let missing = width - text.length()
  if missing <= 0 {
    text
  } else {
    repeat_index_char(' ', missing) + text
  }
}

///|
fn prefix_for_line(line : Int, width : Int) -> String {
  pad_left_number(line, width) + " | "
}

///|
pub fn LineIndex::line_preview(self : LineIndex, line : Int) -> String {
  match self.line_at(line) {
    Some(slice) => visible(slice.text)
    None => ""
  }
}

///|
pub fn LineIndex::line_with_number(self : LineIndex, line : Int) -> String {
  let width = line_number_width(self.line_count())
  match self.line_at(line) {
    Some(slice) => prefix_for_line(line, width) + visible(slice.text)
    None => prefix_for_line(line, width) + ""
  }
}

///|
fn bounded_context_start(line : Int, context : Int) -> Int {
  let start = line - context
  if start < 1 {
    1
  } else {
    start
  }
}

///|
fn bounded_context_end(line : Int, context : Int, max_line : Int) -> Int {
  let end = line + context
  if end > max_line {
    max_line
  } else {
    end
  }
}

///|
pub fn LineIndex::window_at(
  self : LineIndex,
  offset : Int,
  context? : Int = 1,
) -> String {
  let position = self.position_at(offset)
  let safe_context = if context < 0 { 0 } else { context }
  let start = bounded_context_start(position.line, safe_context)
  let end = bounded_context_end(position.line, safe_context, self.line_count())
  let width = line_number_width(self.line_count())
  let out = StringBuilder::new()
  let mut line = start
  while line <= end {
    guard self.line_at(line) is Some(slice) else {
      line += 1
      continue
    }
    out.write_string(prefix_for_line(line, width))
    out.write_string(visible(slice.text))
    out.write_char('\n')
    if line == position.line {
      out.write_string(repeat_index_char(' ', width))
      out.write_string(" | ")
      out.write_string(caret_for_column(position.column))
      out.write_char('\n')
    }
    line += 1
  }
  out.to_string()
}

///|
pub fn line_window(source : String, offset : Int, context? : Int = 1) -> String {
  build_line_index(source).window_at(offset, context~)
}

///|
pub fn location_string(source : String, offset : Int) -> String {
  build_line_index(source).position_at(offset).format()
}

///|
pub fn compact_location_string(source : String, offset : Int) -> String {
  build_line_index(source).position_at(offset).compact()
}

///|
fn make_located_finding(index : LineIndex, finding : Finding) -> LocatedFinding {
  let position = index.position_at(finding.offset)
  let line = index.line_for_offset(finding.offset)
  {
    finding,
    position,
    line_text: line.text,
    caret: caret_for_column(position.column),
  }
}

///|
pub fn locate_findings(
  source : String,
  findings : Array[Finding],
) -> Array[LocatedFinding] {
  let index = build_line_index(source)
  let located : Array[LocatedFinding] = []
  for finding in findings {
    located.push(make_located_finding(index, finding))
  }
  located
}

///|
pub fn AuditResult::located(
  self : AuditResult,
  source : String,
) -> Array[LocatedFinding] {
  locate_findings(source, self.findings)
}

///|
pub fn LocatedFinding::summary(self : LocatedFinding) -> String {
  "\{self.position.compact()} " +
  "\{severity_name(self.finding.severity)} " +
  "\{self.finding.rule_id}: " +
  self.finding.message
}

///|
pub fn LocatedFinding::markdown_row(self : LocatedFinding) -> String {
  "| \{self.position.line} | \{self.position.column} | " +
  markdown_escape(severity_name(self.finding.severity)) +
  " | " +
  markdown_escape(self.finding.rule_id) +
  " | " +
  markdown_escape(risk_name(self.finding.risk)) +
  " | `" +
  markdown_escape(self.finding.sequence) +
  "` | " +
  markdown_escape(self.finding.message) +
  " |"
}

///|
pub fn located_findings_text(located : Array[LocatedFinding]) -> String {
  let out = StringBuilder::new()
  for item in located {
    out.write_string(item.summary())
    out.write_char('\n')
    out.write_string(visible(item.line_text))
    out.write_char('\n')
    out.write_string(item.caret)
    out.write_char('\n')
  }
  out.to_string()
}

///|
pub fn located_findings_markdown(located : Array[LocatedFinding]) -> String {
  let out = StringBuilder::new()
  out.write_string(
    "| Line | Column | Severity | Rule | Risk | Sequence | Description |\n",
  )
  out.write_string("|---:|---:|---|---|---|---|---|\n")
  for item in located {
    out.write_string(item.markdown_row())
    out.write_char('\n')
  }
  out.to_string()
}

///|
pub fn located_findings_json(located : Array[LocatedFinding]) -> String {
  let out = StringBuilder::new()
  out.write_char('[')
  let mut first = true
  for item in located {
    if !first {
      out.write_char(',')
    }
    first = false
    out.write_string("{\"line\":")
    out.write_string(item.position.line.to_string())
    out.write_string(",\"column\":")
    out.write_string(item.position.column.to_string())
    out.write_string(",\"offset\":")
    out.write_string(item.position.offset.to_string())
    out.write_string(",\"rule_id\":")
    out.write_string(json_quote(item.finding.rule_id))
    out.write_string(",\"severity\":")
    out.write_string(json_quote(severity_name(item.finding.severity)))
    out.write_string(",\"risk\":")
    out.write_string(json_quote(risk_name(item.finding.risk)))
    out.write_string(",\"sequence\":")
    out.write_string(json_quote(item.finding.sequence))
    out.write_string(",\"line_text\":")
    out.write_string(json_quote(item.line_text))
    out.write_string("}")
  }
  out.write_char(']')
  out.to_string()
}

///|
pub fn LineIndex::range_for_event(
  self : LineIndex,
  event : Event,
) -> SourceRange {
  let start = self.position_at(event.offset)
  let finish = self.position_at(event.offset + event.raw.iter().count())
  { start, end: finish }
}

///|
pub fn LineIndex::range_for_finding(
  self : LineIndex,
  finding : Finding,
) -> SourceRange {
  let start = self.position_at(finding.offset)
  let finish = self.position_at(
    finding.offset + finding.sequence.iter().count(),
  )
  { start, end: finish }
}

///|
pub fn SourceRange::format(self : SourceRange) -> String {
  if self.start.line == self.end.line {
    "line \{self.start.line}, columns \{self.start.column}-\{self.end.column}"
  } else {
    "\{self.start.compact()}..\{self.end.compact()}"
  }
}

///|
pub fn line_index_summary(index : LineIndex) -> String {
  "\{index.line_count()} line(s), \{index.chars} character(s)"
}