//===--- BacktraceFormatter.swift -----------------------------*- swift -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
//  Provides functionality to format backtraces, with various additional
//  options.
//
//===----------------------------------------------------------------------===//

import Swift

#if os(anyAppleOS)
internal import Darwin
@_implementationOnly import BacktracingImpl.OS.Darwin
#elseif os(Windows)
internal import ucrt
#elseif canImport(Glibc)
internal import Glibc
#elseif canImport(Musl)
internal import Musl
#endif

/// A backtrace formatting theme.
@_spi(Formatting)
@available(BacktracingDT 6.2, *)
public protocol BacktraceFormattingTheme {
  func frameIndex(_ s: String) -> String
  func programCounter(_ s: String) -> String
  func frameAttribute(_ s: String) -> String
  func symbol(_ s: String) -> String
  func offset(_ s: String) -> String
  func sourceLocation(_ s: String) -> String
  func lineNumber(_ s: String) -> String
  func code(_ s: String) -> String
  func crashedLineNumber(_ s: String) -> String
  func crashedLine(_ s: String) -> String
  func crashLocation() -> String
  func imageName(_ s: String) -> String
  func imageAddressRange(_ s: String) -> String
  func imageBuildID(_ s: String) -> String
  func imagePath(_ s: String) -> String
}

@available(BacktracingDT 6.2, *)
extension BacktraceFormattingTheme {
  public func frameIndex(_ s: String) -> String { return s }
  public func programCounter(_ s: String) -> String { return s }
  public func frameAttribute(_ s: String) -> String { return "[\(s)]" }
  public func symbol(_ s: String) -> String { return s }
  public func offset(_ s: String) -> String { return s }
  public func sourceLocation(_ s: String) -> String { return s }
  public func lineNumber(_ s: String) -> String { return " \(s)|" }
  public func code(_ s: String) -> String { return s }
  public func crashedLineNumber(_ s: String) -> String { return "*\(s)|" }
  public func crashedLine(_ s: String) -> String { return s }
  public func crashLocation() -> String { return "^" }
  public func imageName(_ s: String) -> String { return s }
  public func imageAddressRange(_ s: String) -> String { return s }
  public func imageBuildID(_ s: String) -> String { return s }
  public func imagePath(_ s: String) -> String { return s}
}

/// Options for backtrace formatting.
///
/// This is used by chaining modifiers, e.g. .theme(.color).showSourceCode().
@_spi(Formatting)
@available(BacktracingDT 6.2, *)
public struct BacktraceFormattingOptions {
  var _theme: BacktraceFormattingTheme = BacktraceFormatter.Themes.plain
  var _showSourceCode: Bool = false
  var _sourceContextLines: Int = 2
  var _showAddresses: Bool = true
  var _showImages: ImagesToShow = .mentioned
  var _showImageNames: Bool = true
  var _showFrameAttributes: Bool = true
  var _skipRuntimeFailures: Bool = false
  var _skipThunkFunctions: Bool = true
  var _skipSystemFrames: Bool = true
  var _sanitizePaths: Bool = true
  var _demangle: Bool = true
  var _width: Int = 80

  public var selectedTheme: BacktraceFormattingTheme { return _theme }
  public var shouldShowSourceCode: Bool { return _showSourceCode }
  public var sourceContextLines: Int { return _sourceContextLines }
  public var shouldShowAddresses: Bool { return _showAddresses }
  public var imagesToShow: ImagesToShow { return _showImages }
  public var shouldShowImageNames: Bool { return _showImageNames }
  public var shouldShowFrameAttributes: Bool { return _showFrameAttributes }
  public var shouldSkipRuntimeFailures: Bool { return _skipRuntimeFailures }
  public var shouldSkipThunkFunctions: Bool { return _skipThunkFunctions }
  public var shouldSkipSystemFrames: Bool { return _skipSystemFrames }
  public var shouldSanitizePaths: Bool { return _sanitizePaths }
  public var shouldDemangle: Bool { return _demangle }
  public var formattingWidth: Int { return _width }

  public init() {}

  /// Theme to use for formatting.
  ///
  /// - Parameters:
  ///   - theme: A ``BacktraceFormattingTheme`` structure.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func theme(_ theme: BacktraceFormattingTheme) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().theme(theme)
  }
  public func theme(_ theme: BacktraceFormattingTheme) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._theme = theme
    return newOptions
  }

  /// Enable or disable the display of source code in the backtrace.
  ///
  /// - Parameters:
  ///   - enabled: Whether or not to enable source code.
  ///
  ///   - contextLines: The number of lines of context either side of the
  ///                      line associated with the backtrace frame.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func showSourceCode(_ enabled: Bool = true, contextLines: Int = 2) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().showSourceCode(enabled,
                                                       contextLines: contextLines)
  }
  public func showSourceCode(_ enabled: Bool = true, contextLines: Int = 2) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._showSourceCode = enabled
    newOptions._sourceContextLines = contextLines
    return newOptions
  }

  /// Enable or disable the display of raw addresses.
  ///
  /// - Parameters:
  ///   - enabled: If false, we will only display a raw address in the
  ///                 backtrace if we haven't been able to symbolicate.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func showAddresses(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().showAddresses(enabled)
  }
  public func showAddresses(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._showAddresses = enabled
    return newOptions
  }

  /// Enable or disable the display of the image list.
  ///
  /// - Parameters: 
  ///   - enabled: Says whether or not to output the image list.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public enum ImagesToShow {
    case none
    case mentioned
    case all
  }
  public static func showImages(_ toShow: ImagesToShow = .all) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().showImages(toShow)
  }
  public func showImages(_ toShow: ImagesToShow = .all) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._showImages = toShow
    return newOptions
  }

  /// Enable or disable the display of image names in the frame list.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will display the name of the image for
  ///                 each frame.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func showImageNames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().showImageNames(enabled)
  }

  /// Enable or disable the display of image names in the frame list.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will display the name of the image for
  ///                 each frame.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public func showImageNames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._showImageNames = enabled
    return newOptions
  }

  /// Enable or disable the display of frame attributes in the frame list.
  ///
  /// - Parameters
  ///   - enabled: If true, we will display the frame attributes.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func showFrameAttributes(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().showFrameAttributes(enabled)
  }

  /// Enable or disable the display of frame attributes in the frame list.
  ///
  /// - Parameters
  ///   - enabled: If true, we will display the frame attributes.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public func showFrameAttributes(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._showFrameAttributes = enabled
    return newOptions
  }

  /// Set whether or not to show Swift runtime failure frames.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will skip Swift runtime failure frames.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func skipRuntimeFailures(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().skipRuntimeFailures(enabled)
  }

  /// Set whether or not to show Swift runtime failure frames.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will skip Swift runtime failure frames.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public func skipRuntimeFailures(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._skipRuntimeFailures = enabled
    return newOptions
  }

  /// Set whether or not to show Swift thunk function frames.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will skip Swift thunk function frames.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func skipThunkFunctions(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().skipThunkFunctions(enabled)
  }

  /// Set whether or not to show Swift thunk function frames.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will skip Swift thunk function frames.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public func skipThunkFunctions(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._skipThunkFunctions = enabled
    return newOptions
  }

  /// Set whether or not to show system frames.
  ///
  /// For instance, on macOS, this will cause us to skip the "start" frame
  /// at the very top of the stack.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will skip system frames.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func skipSystemFrames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().skipSystemFrames(enabled)
  }

  /// Set whether or not to show system frames.
  ///
  /// For instance, on macOS, this will cause us to skip the "start" frame
  /// at the very top of the stack.
  ///
  /// - Parameters:
  ///   - enabled: If true, we will skip system frames.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public func skipSystemFrames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._skipSystemFrames = enabled
    return newOptions
  }

  /// Enable or disable path sanitization.
  ///
  /// This is intended to avoid leaking PII into crash logs.
  ///
  /// - Parameters:
  ///   - enabled:  If true, paths will be sanitized.
  ///
  /// - Returns: A new `BacktraceFormattingOptions` structure.
  public static func sanitizePaths(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().sanitizePaths(enabled)
  }

  /// Enable or disable path sanitization.
  ///
  /// This is intended to avoid leaking PII into crash logs.
  ///
  /// - Parameters:
  ///   - enabled:  If true, paths will be sanitized.
  ///
  /// - Returns: A new `BacktraceFormattingOptions` structure.
  public func sanitizePaths(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._sanitizePaths = enabled
    return newOptions
  }

  /// Set whether we show mangled or demangled names.
  ///
  /// - Parameters:
  ///   - enabled: If true, we show demangled names if we have them.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func demangle(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().demangle(enabled)
  }
  public func demangle(_ enabled: Bool = true) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._demangle = enabled
    return newOptions
  }

  /// Set the output width.
  ///
  /// - Parameters:
  ///   - width: The output width in characters.  This is only used to
  ///               highlight information, and defaults to 80.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public static func width(_ width: Int) -> BacktraceFormattingOptions {
    return BacktraceFormattingOptions().width(width)
  }

  /// Set the output width.
  ///
  /// - Parameters:
  ///   - width: The output width in characters.  This is only used to
  ///               highlight information, and defaults to 80.
  ///
  /// - Returns: A new ``BacktraceFormattingOptions`` structure.
  public func width(_ width: Int) -> BacktraceFormattingOptions {
    var newOptions = self
    newOptions._width = width
    return newOptions
  }
}

/// Return the width of a given Unicode.Scalar.
///
/// It would be nice to have the Unicode width data, which would let us do
/// a better job of this.
private func measure(_ ch: Unicode.Scalar) -> Int {
  if ch.isASCII {
    return 1
  }

  if ch.properties.isEmoji {
    return 2
  }

  if ch.properties.isIdeographic
       && !(ch.value >= 0xff61 && ch.value <= 0xffdc)
       && !(ch.value >= 0xffe8 && ch.value <= 0xffee) {
    return 2
  }

  if ch.properties.canonicalCombiningClass.rawValue != 0 {
    return 0
  }

  switch ch.properties.generalCategory {
    case .control, .nonspacingMark:
      return 0
    default:
      return 1
  }
}

/// Compute the width of the given string, ignoring CSI formatting codes.
private enum MeasureState {
  // Normal state
  case normal

  // Start of an escape
  case escape

  // In a CSI escape
  case csi
}

private func measure<S: StringProtocol>(_ s: S) -> Int {
  var totalWidth = 0
  var state: MeasureState = .normal

  for ch in s.unicodeScalars {
    switch state {
      case .normal:
        if ch.value == 27 {
          // This is an escape sequence
          state = .escape
        } else {
          totalWidth += measure(ch)
        }
      case .escape:
        if ch.value == 0x5b {
          state = .csi
        } else {
          state = .normal
        }
      case .csi:
        if ch.value >= 0x40 && ch.value <= 0x7e {
          state = .normal
        }
    }
  }
  return totalWidth
}

/// Pad the given string to the given width using spaces.
@available(BacktracingDT 6.2, *)
private func pad(_ s: String, to width: Int,
                 aligned alignment: BacktraceFormatter.Alignment = .left)
  -> String {

  let currentWidth = measure(s)
  let padding = max(width - currentWidth, 0)

  switch alignment {
    case .left:
      let spaces = String(repeating: " ", count: padding)

      return s + spaces
    case .right:
      let spaces = String(repeating: " ", count: padding)

      return spaces + s
    case .center:
      let left = padding / 2
      let right = padding - left
      let leftSpaces = String(repeating: " ", count: left)
      let rightSpaces = String(repeating: " ", count: right)

      return "\(leftSpaces)\(s)\(rightSpaces)"
  }
}

/// Untabify the given string, assuming tabs of the specified size.
///
/// - Parameters:
///   - s: The string to untabify.
///   - tabWidth: The tab width to assume (default 8).
///
/// - Returns: A string with all the tabs replaced with appropriate numbers
///          of spaces.
private func untabify(_ s: String, tabWidth: Int = 8) -> String {
  var result: String = ""
  var first = true
  for chunk in s.split(separator: "\t", omittingEmptySubsequences: false) {
    if first {
      first = false
    } else {
      let toTabStop = tabWidth - measure(result) % tabWidth
      result += String(repeating: " ", count: toTabStop)
    }
    result += chunk
  }
  return result
}

/// Sanitize a path to remove usernames, volume names and so on.
///
/// The point of this function is to try to remove anything that might
/// contain PII before it ends up in a log file somewhere.
///
/// - Parameters:
///   - path: The path to sanitize.
///
/// - Returns: A string containing the sanitized path.
@_spi(Formatting)
public func sanitizePath(_ path: String) -> String {
  #if os(macOS)
  return CRCopySanitizedPath(path,
                             kCRSanitizePathGlobAllTypes
                               | kCRSanitizePathKeepFile)
  #else
  // For now, on non-macOS systems, do nothing
  return path
  #endif
}

/// Trim whitespace from the right hand end of a string.
///
/// - Parameters:
///   - s: The string to trim.
///
/// - Returns: A string with the whitespace trimmed.
private func rtrim<S: StringProtocol>(_ s: S) -> S.SubSequence {
  if let lastNonWhitespace = s.lastIndex(where: { !$0.isWhitespace }) {
    return s.prefix(through: lastNonWhitespace)
  }
  return s.dropLast(0)
}

/// Responsible for formatting backtraces.
@_spi(Formatting)
@available(BacktracingDT 6.2, *)
public struct BacktraceFormatter {

  /// The formatting options to apply when formatting data.
  public var options: BacktraceFormattingOptions

  public struct Themes {
    /// A plain formatting theme.
    public struct PlainTheme: BacktraceFormattingTheme {
    }

    public static let plain = PlainTheme()
  }

  public init(_ options: BacktraceFormattingOptions) {
    self.options = options
  }

  public enum TableRow {
    case columns([String])
    case raw(String)
  }

  public enum Alignment {
    case left
    case right
    case center
  }

  /// Output a table with each column nicely aligned.
  ///
  /// - Parameters:
  ///   - rows  An array of table rows, each of which holds an array
  ///              of table columns.
  ///
  /// - Returns: A `String` containing the formatted table.
  public static func formatTable(_ rows: [TableRow],
                                  alignments: [Alignment] = []) -> String {
    // Work out how many columns we have
    let colCount = rows.map{
      if case let .columns(columns) = $0 {
        return columns.count
      } else {
        return 0
      }
    }.reduce(0, max)

    // Now compute their widths
    var widths = Array(repeating: 0, count: colCount)
    for row in rows {
      if case let .columns(columns) = row {
        for (n, width) in columns.lazy.map(measure).enumerated() {
          widths[n] = max(widths[n], width)
        }
      }
    }

    // Generate lines for the table
    var lines: [Substring] = []
    for row in rows {
      switch row {
        case let .columns(columns):
          let line = columns.enumerated().map{ n, column in
            let alignment = n < alignments.count ? alignments[n] : .left
            if n == colCount - 1 && alignment == .left {
              return column
            } else {
              return pad(column, to: widths[n], aligned: alignment)
            }
          }.joined(separator: " ")

          lines.append(rtrim(line))
        case let .raw(line):
          lines.append(rtrim(line))
      }
    }

    // Trim any empty lines from the end
    guard let lastNonEmpty = lines.lastIndex(where: { !$0.isEmpty }) else {
      return ""
    }

    return lines.prefix(through: lastNonEmpty).joined(separator: "\n")
  }

  /// Format an individual frame into a list of columns.
  ///
  /// - Parameters:
  ///   - frame: The frame to format.
  ///   - index: The frame index, if required.
  ///
  /// - Returns: An array of strings, one per column.
  public func formatColumns(frame: Backtrace.Frame,
                            index: Int? = nil) -> [String] {
    let pc: String
    var attrs: [String] = []

    switch frame {
      case let .programCounter(address):
        pc = "\(address)"
      case let .returnAddress(address):
        pc = "\(address)"
        attrs.append("ra")
      case let .asyncResumePoint(address):
        pc = "\(address)"
        attrs.append("async")
      case .omittedFrames(_), .truncated:
        pc = "..."
    }

    var columns: [String] = []
    if let index = index {
      columns.append(options._theme.frameIndex("\(index)"))
    }
    if options._showFrameAttributes {
      columns.append(attrs.map(
                       options._theme.frameAttribute
                     ).joined(separator: " "))
    }
    columns.append(options._theme.programCounter(pc))

    return columns
  }

  /// Format a frame into a list of rows.
  /// - Parameters:
  ///   - frame: The frame to format.
  ///   - index: The frame index, if required.
  ///
  /// - Returns: An array of table rows.
  public func formatRows(frame: Backtrace.Frame,
                         index: Int? = nil) -> [TableRow] {
    return [.columns(formatColumns(frame: frame,
                                   index: index))]
  }

  /// Format just one frame.
  ///
  /// - Parameters:
  /// - frame: The frame to format.
  /// - index: The frame index, if required.
  ///
  /// - Returns: A `String` containing the formatted data.
  public func format(frame: Backtrace.Frame,
                     index: Int? = nil) -> String {
    let rows = formatRows(frame: frame,
                          index: index)
    return BacktraceFormatter.formatTable(rows, alignments: [.right])
  }

  /// Format the frame list from a backtrace.
  ///
  /// - Parameters:
  ///   - frames: The frames to format (lazy loaded Sequence).
  ///
  /// - Returns: A `String` containing the formatted data.
  public func format(frames: some Sequence<Backtrace.Frame>) -> String {
    var rows: [TableRow] = []

    var n = 0
    for frame in frames {
      rows += formatRows(frame: frame, index: n)

      if case let .omittedFrames(count) = frame {
        n += count
      } else {
        n += 1
      }
    }

    return BacktraceFormatter.formatTable(rows, alignments: [.right])
  }

  /// Format a `Backtrace`
  ///
  /// - Parameters:
  ///   - backtrace: The `Backtrace` object to format.
  ///
  /// - Returns: A `String` containing the formatted data.
  public func format(backtrace: Backtrace) -> String {
    return format(frames: backtrace.frames)
  }

  /// Grab source lines for a symbolicated backtrace.
  ///
  /// Tries to open the file corresponding to the symbol; if successful,
  /// it will return a string containing the specified lines of context,
  /// with the point at which the program crashed highlighted.
  private func formattedSourceLines(from sourceLocation: SymbolicatedBacktrace.SourceLocation,
                                    indent theIndent: Int = 2) -> String? {
    #if os(Windows)
    var fp: UnsafeMutablePointer<FILE>? = nil
    let err = fopen_s(&fp, sourceLocation.path, "rt")
    if err != 0 {
      return nil
    }
    #else
    guard let fp = fopen(sourceLocation.path, "rt") else {
      return nil
    }
    #endif
    defer {
      fclose(fp)
    }

    let indent = String(repeating: " ", count: theIndent)
    var lines: [String] = []
    var line = 1
    let buffer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: 4096)
    var currentLine = ""

    let maxLine = sourceLocation.line + options._sourceContextLines
    let maxLineWidth = max("\(maxLine)".count, 4)

    let doLine = { sourceLine in
      if line >= sourceLocation.line - options._sourceContextLines
           && line <= sourceLocation.line + options._sourceContextLines {
        let untabified = untabify(sourceLine)
        let code = options._theme.code(untabified)
        let theLine: String
        if line == sourceLocation.line {
          let lineNumber = options._theme.crashedLineNumber(pad("\(line)",
                                                                to: maxLineWidth,
                                                                aligned: .right))
          let highlightWidth = options._width - 2 * theIndent
          theLine = options._theme.crashedLine(pad("\(lineNumber) \(code)",
                                                   to: highlightWidth))
        } else {
          let lineNumber = options._theme.lineNumber(pad("\(line)",
                                                         to: maxLineWidth,
                                                         aligned: .right))
          theLine = "\(lineNumber) \(code)"
        }
        lines.append("\(indent)\(theLine)")

        if line == sourceLocation.line {
          // sourceLocation.column is an index in UTF-8 code units in
          // `untabified`.  We should point at the grapheme cluster that
          // contains that UTF-8 index.
          let adjustedColumn: Int
          if sourceLocation.column > 0 {
            adjustedColumn = sourceLocation.column
          } else {
            if let ndx = code.firstIndex(where: { $0 != " " }) {
              adjustedColumn = code.distance(from: code.startIndex, to: ndx) + 1
            } else {
              adjustedColumn = 1
            }
          }
          let utf8Ndx
            = untabified.utf8.index(untabified.utf8.startIndex,
                                    offsetBy: adjustedColumn,
                                    limitedBy: untabified.utf8.endIndex)
            ?? untabified.utf8.endIndex

          // Adjust it to point at a grapheme cluster start
          let strNdx = untabified.index(
            untabified.index(utf8Ndx, offsetBy: 1,
                             limitedBy: untabified.endIndex)
              ?? untabified.endIndex,
            offsetBy: -1,
            limitedBy: untabified.startIndex) ?? untabified.startIndex

          // Work out the terminal width up to that point
          let terminalWidth = measure(untabified.prefix(upTo: strNdx))

          let pad = String(repeating: " ",
                           count: max(terminalWidth - 1, 0))

          let marker = options._theme.crashLocation()
          let blankForNumber = options._theme.lineNumber(
            String(repeating: " ", count: maxLineWidth))

          lines.append("\(indent)\(blankForNumber) \(pad)\(marker)")
        }
      }
    }

    while feof(fp) == 0 && ferror(fp) == 0 {
      guard let result = fgets(buffer.baseAddress,
                               CInt(buffer.count), fp) else {
        break
      }

      let chunk = String(cString: result)
      currentLine += chunk
      if currentLine.hasSuffix("\n") {
        currentLine.removeLast()
        doLine(currentLine)
        currentLine = ""
        line += 1
      }
    }

    doLine(currentLine)

    return lines.joined(separator: "\n")
  }

  /// Format an individual frame into a list of columns.
  ///
  /// - Parameters:
  ///   - frame: The frame to format.
  ///
  /// - Returns: An array of strings, one per column.
  public func formatColumns(frame: SymbolicatedBacktrace.Frame,
                            index: Int? = nil) -> [String] {
    let pc: String
    var attrs: [String] = []

    switch frame.captured {
      case let .programCounter(address):
        pc = "\(address)"
      case let .returnAddress(address):
        pc = "\(address)"
        attrs.append("ra")
      case let .asyncResumePoint(address):
        pc = "\(address)"
        attrs.append("async")
      case .omittedFrames(_), .truncated:
        pc = ""
    }

    if frame.inlined {
      attrs.append("inlined")
    }

    if frame.isSwiftThunk {
      attrs.append("thunk")
    }

    if frame.isSystem {
      attrs.append("system")
    }

    var formattedSymbol: String? = nil
    var hasSourceLocation = false

    if let symbol = frame.symbol {
      let displayName = options._demangle ? symbol.name : symbol.rawName
      let themedName = options._theme.symbol(displayName)

      let offset: String
      if symbol.offset > 0 {
        offset = options._theme.offset(" + \(symbol.offset)")
      } else if symbol.offset < 0 {
        offset = options._theme.offset(" - \(-symbol.offset)")
      } else {
        offset = ""
      }

      let imageName: String
      if options._showImageNames {
        if symbol.imageIndex >= 0 {
          imageName = " in " + options._theme.imageName(symbol.imageName)
        } else {
          imageName = ""
        }
      } else {
        imageName = ""
      }

      let location: String
      if var sourceLocation = symbol.sourceLocation {
        if options._sanitizePaths {
          sourceLocation.path = sanitizePath(sourceLocation.path)
        }
        location = " at " + options._theme.sourceLocation("\(sourceLocation)")
        hasSourceLocation = true
      } else {
        location = ""
      }

      formattedSymbol = "\(themedName)\(offset)\(imageName)\(location)"
    }

    let location: String
    if !hasSourceLocation || options._showAddresses {
      let formattedPc = options._theme.programCounter(pc)
      if let formattedSymbol = formattedSymbol {
        location = "\(formattedPc) \(formattedSymbol)"
      } else {
        location = formattedPc
      }
    } else if let formattedSymbol = formattedSymbol {
      location = formattedSymbol
    } else {
      location = options._theme.programCounter(pc)
    }

    var columns: [String] = []

    if let index = index {
      let frameIndex: String
      switch frame.captured {
        case .omittedFrames(_), .truncated:
          frameIndex = options._theme.frameIndex("...")
        default:
          frameIndex = options._theme.frameIndex("\(index)")
      }
      columns.append(frameIndex)
    }

    if options._showFrameAttributes {
      columns.append(attrs.map(
                       options._theme.frameAttribute
                     ).joined(separator: " "))
    }

    columns.append(location)

    return columns
  }

  /// Format a frame into a list of rows.
  ///
  /// - Parameters:
  ///   - frame: The frame to format.
  ///   - index: The frame index, if required.
  ///
  /// - Returns: An array of table rows.
  public func formatRows(frame: SymbolicatedBacktrace.Frame,
                         index: Int? = nil,
                         showSource: Bool = true) -> [TableRow] {
    let columns = formatColumns(frame: frame,
                                index: index)
    var rows: [TableRow] = [.columns(columns)]

    if showSource {
      if let symbol = frame.symbol,
         let sourceLocation = symbol.sourceLocation,
         let lines = formattedSourceLines(from: sourceLocation) {
        rows.append(.raw(""))
        rows.append(.raw(lines))
        rows.append(.raw(""))
      }
    }

    return rows
  }

  /// Format just one frame.
  ///
  /// - Parameters:
  ///   - frame: The frame to format.
  ///   - index: The frame index, if required.
  ///
  /// - Returns: A `String` containing the formatted data.
  public func format(frame: SymbolicatedBacktrace.Frame,
                     index: Int? = nil,
                     showSource: Bool = true) -> String {
    let rows = formatRows(frame: frame, index: index, showSource: showSource)
    return BacktraceFormatter.formatTable(rows, alignments: [.right])
  }

  /// Return `true` if we should skip the specified frame
  public func shouldSkip(_ frame: SymbolicatedBacktrace.Frame) -> Bool {
    return (options._skipRuntimeFailures && frame.isSwiftRuntimeFailure)
      || (options._skipSystemFrames && frame.isSystem)
      || (options._skipThunkFunctions && frame.isSwiftThunk)
  }

  /// Format the frame list from a symbolicated backtrace.
  /// 
  /// - Parameters:
  ///   - frames: The frames to format (lazy loaded Sequence).
  ///
  /// - Returns: A `String` containing the formatted data.
  public func format(
    frames: some Sequence<SymbolicatedBacktrace.Frame>
  ) -> String {
    var rows: [TableRow] = []
    var sourceLocationsShown = Set<SymbolicatedBacktrace.SourceLocation>()

    var n = 0
    for frame in frames {
      if shouldSkip(frame) {
        continue
      }

      var showSource = options._showSourceCode
      if let symbol = frame.symbol,
         let sourceLocation = symbol.sourceLocation {
        if sourceLocationsShown.contains(sourceLocation) {
          showSource = false
        } else {
          sourceLocationsShown.insert(sourceLocation)
        }
      }

      rows += formatRows(frame: frame, index: n, showSource: showSource)

      if case let .omittedFrames(count) = frame.captured {
        n += count
      } else {
        n += 1
      }
    }

    return BacktraceFormatter.formatTable(rows, alignments: [.right])
  }

  /// Format a ``SymbolicatedBacktrace``
  ///
  /// - Parameters:
  ///   - backtrace: The ``SymbolicatedBacktrace`` object to format.
  ///
  /// - Returns: A `String` containing the formatted data.
  public func format(backtrace: SymbolicatedBacktrace) -> String {
    var result = format(frames: backtrace.frames)

    switch options._showImages {
      case .none:
        break
      case .all:
        result += "\n\nImages:\n"
        result += format(images: backtrace.images)
      case .mentioned:
        var mentionedImages = Set<Int>()
        for frame in backtrace.frames {
          if shouldSkip(frame) {
            continue
          }
          if let symbol = frame.symbol, symbol.imageIndex >= 0 {
            mentionedImages.insert(symbol.imageIndex)
          }
        }

        let images = mentionedImages.sorted().map{ backtrace.images[$0] }
        let omitted = backtrace.images.count - images.count
        if omitted > 0 {
          result += "\n\nImages (\(omitted) omitted):\n"
        } else {
          result += "\n\nImages (only mentioned):\n"
        }
        result += format(images: images)
    }

    return result
  }

  /// Format a ``Backtrace/Image`` into a list of columns.
  ///
  /// - Parameters:
  ///   - image: The `Image` object to format.
  /// 
  /// - Returns: An array of strings, one per column.
  public func formatColumns(image: Backtrace.Image) -> [String] {
    let addressRange = "\(image.baseAddress)–\(image.endOfText)"
    let buildID: String
    if let bytes = image.uniqueID {
      buildID = hex(bytes)
    } else {
      buildID = "<no build ID>"
    }
    let imagePath: String
    if let path = image.path {
      if options._sanitizePaths {
        imagePath = sanitizePath(path)
      } else {
        imagePath = path
      }
    } else {
      imagePath = "<unknown>"
    }
    let imageName = image.name ?? "<unknown>"
    return [
      options._theme.imageAddressRange(addressRange),
      options._theme.imageBuildID(buildID),
      options._theme.imageName(imageName),
      options._theme.imagePath(imagePath)
    ]
  }

  /// Format an array of ``Backtrace/Image``s.
  ///
  /// - Parameters:
  ///   - images: The array of ``Image`` objects to format.
  ///
  /// - Returns: A string containing the formatted data.
  public func format(images: some Sequence<Backtrace.Image>) -> String {
    let rows = images.map{
      TableRow.columns(
        formatColumns(image: $0)
      )
    }

    return BacktraceFormatter.formatTable(rows)
  }
}
