///|
priv struct PreviewWriter {
  output : StringBuilder
  mut remaining : Int32
  mut steps : Int32
  mut stopped : Bool
}

///|
fn PreviewWriter::tick(self : PreviewWriter) -> Unit raise SchemeError {
  self.steps -= 1
  if self.steps < 0 {
    raise Limit("value preview traversal")
  }
}

///|
fn PreviewWriter::character(
  self : PreviewWriter,
  character : Char,
) -> Unit raise SchemeError {
  if self.stopped {
    return
  }
  self.tick()
  let units = if character.to_int() > 65535 { 2 } else { 1 }
  if units > self.remaining {
    self.stopped = true
    return
  }
  self.output.write_char(character)
  self.remaining -= units
  if self.remaining == 0 {
    self.stopped = true
  }
}

///|
fn PreviewWriter::text(
  self : PreviewWriter,
  text : String,
) -> Unit raise SchemeError {
  for character in text {
    if self.stopped {
      break
    }
    self.character(character)
  }
}

///|
fn PreviewWriter::quoted(
  self : PreviewWriter,
  text : String,
  terminator : Char,
) -> Unit raise SchemeError {
  self.character(terminator)
  for character in text {
    if self.stopped {
      break
    }
    if character == terminator || character == '\\' {
      self.character('\\')
      self.character(character)
    } else if character == '\n' {
      self.text("\\n")
    } else if character == '\r' {
      self.text("\\r")
    } else if character == '\t' {
      self.text("\\t")
    } else {
      self.character(character)
    }
  }
  self.character(terminator)
}

///|
fn PreviewWriter::symbol(
  self : PreviewWriter,
  text : String,
) -> Unit raise SchemeError {
  let mut plain = !text.is_empty() && text != "."
  if plain {
    for character in text {
      self.tick()
      if delimiter(character) || character == '|' || character == '\\' {
        plain = false
        break
      }
    }
  }
  if plain {
    let first = text.iter().next().unwrap()
    if first == '#' || (first >= '0' && first <= '9') {
      plain = false
    } else if first == '+' || first == '-' || first == '.' {
      // A number-like identifier needs classification before emitting its first
      // character. Bound this lookahead instead of constructing a huge BigInt.
      if text.length() > 10000 {
        raise Limit("value preview numeric atom")
      }
      let parsed = parse_number(text) catch { _ => None }
      if parsed is Some(_) {
        plain = false
      }
    }
  }
  if plain {
    self.text(text)
  } else {
    self.quoted(text, '|')
  }
}

///|
fn PreviewWriter::number(
  self : PreviewWriter,
  number : Number,
) -> Unit raise SchemeError {
  match number {
    Integer(value) =>
      if value.bit_length() > 65536 {
        raise Limit("value preview numeric atom")
      }
    Rational(numerator, denominator) =>
      if numerator.bit_length() > 65536 || denominator.bit_length() > 65536 {
        raise Limit("value preview numeric atom")
      }
    Real(_) => ()
  }
  self.text(number.to_text())
}

///|
fn PreviewWriter::value(
  self : PreviewWriter,
  value : Value,
  depth : Int32,
) -> Unit raise SchemeError {
  if self.stopped {
    return
  }
  self.tick()
  if depth > 256 {
    raise Limit("value printing depth")
  }
  match value {
    Number(number) => self.number(number)
    Boolean(value) => self.text(if value { "#t" } else { "#f" })
    Character(character) => {
      self.text("#\\")
      match character {
        ' ' => self.text("space")
        '\n' => self.text("newline")
        '\t' => self.text("tab")
        '\r' => self.text("return")
        _ => self.character(character)
      }
    }
    Text(text) => self.quoted(text, '"')
    Symbol(text) => self.symbol(text)
    Nil => self.text("()")
    Pair(_, _) => {
      self.text("(")
      let mut rest = value
      let mut first = true
      while rest is Pair(head, tail) {
        if self.stopped {
          return
        }
        self.tick()
        if !first {
          self.text(" ")
        }
        self.value(head, depth + 1)
        first = false
        rest = tail
      }
      match rest {
        Nil => ()
        _ => {
          self.text(" . ")
          self.value(rest, depth + 1)
        }
      }
      self.text(")")
    }
    Vector(items) => {
      self.text("#(")
      for index, item in items {
        if self.stopped {
          return
        }
        if index > 0 {
          self.text(" ")
        }
        self.value(item, depth + 1)
      }
      self.text(")")
    }
    Record(fields) => {
      self.text("#")
    }
    Tagged(tag, items) => {
      self.text("#<")
      self.text(tag)
      self.text(" ")
      for index, item in items {
        if self.stopped {
          return
        }
        if index > 0 {
          self.text(" ")
        }
        self.value(item, depth + 1)
      }
      self.text(">")
    }
    Unit => self.text("#")
    Closure(_) | Native(_) | NativeFunction(_) => self.text("#")
    IR(_) => self.text("#")
    IRType(_) => self.text("#")
    IRFile(_) => self.text("#")
  }
}

///|
/// Return a UTF-16-safe prefix of the existing value printer without expanding
/// the rest of a composite value. Limits are 0..65536 units, depth 256 and
/// 100000 traversal steps. Excessive lookahead or numeric atoms raise Limit.
pub fn value_preview(
  value : Value,
  max_units : Int32,
) -> String raise SchemeError {
  if max_units < 0 || max_units > 65536 {
    raise Invalid("Value preview limit must be between 0 and 65536")
  }
  let writer = PreviewWriter::{
    output: StringBuilder(),
    remaining: max_units,
    steps: 100000,
    stopped: max_units == 0,
  }
  writer.value(value, 0)
  writer.output.to_string()
}

///|
// Diagnostic arguments share one streaming budget, including separators. Never
// fully render an argument before limiting the aggregate error detail.
fn error_arguments_preview(
  arguments : Array[Value],
) -> String raise SchemeError {
  let writer = PreviewWriter::{
    output: StringBuilder(),
    remaining: 4096,
    steps: 100000,
    stopped: false,
  }
  for index, argument in arguments {
    if writer.stopped {
      break
    }
    if index > 0 {
      writer.text(" ")
    }
    writer.value(argument, 0)
  }
  writer.output.to_string()
}