///|
/// Append a text-leading operation.
pub fn PdfDrawControl::add_leading(
  self : PdfDrawControl,
  leading : Double,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawLeading(leading))
}

///|
/// Append a character-spacing operation.
pub fn PdfDrawControl::add_char_space(
  self : PdfDrawControl,
  spacing : Double,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawCharSpace(spacing))
}

///|
/// Append a word-spacing operation.
pub fn PdfDrawControl::add_word_space(
  self : PdfDrawControl,
  spacing : Double,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawWordSpace(spacing))
}

///|
/// Append a horizontal text-scale operation.
pub fn PdfDrawControl::add_text_scale(
  self : PdfDrawControl,
  scale : Double,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawTextScale(scale))
}

///|
/// Append a text rendering-mode operation.
pub fn PdfDrawControl::add_render_mode(
  self : PdfDrawControl,
  mode : Int,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawRenderMode(mode))
}

///|
/// Append a text-rise operation.
pub fn PdfDrawControl::add_rise(
  self : PdfDrawControl,
  rise : Double,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawRise(rise))
}

///|
/// Append a text-newline operation.
pub fn PdfDrawControl::add_newline(
  self : PdfDrawControl,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawNewline)
}

///|
/// Set the font name used by subsequent text operations.
pub fn PdfDrawControl::set_font_name(
  self : PdfDrawControl,
  font_name : String,
) -> Unit {
  self.font_name = font_name
}

///|
/// Return the current draw-control font name.
///
/// This is the per-instance counterpart of `cpdfdrawcontrol.ml`'s
/// `getfontname` callback.
pub fn PdfDrawControl::font_name(self : PdfDrawControl) -> String {
  self.font_name
}

///|
/// Set the font size used by subsequent text operations.
pub fn PdfDrawControl::set_font_size(
  self : PdfDrawControl,
  font_size : Double,
) -> Unit {
  self.font_size = font_size
}

///|
/// Return the current draw-control font size.
///
/// This is the per-instance counterpart of `cpdfdrawcontrol.ml`'s
/// `getfontsize` callback.
pub fn PdfDrawControl::font_size(self : PdfDrawControl) -> Double {
  self.font_size
}

///|
/// Set the font name and size used by subsequent text operations.
pub fn PdfDrawControl::set_font(
  self : PdfDrawControl,
  font_name : String,
  font_size : Double,
) -> Unit {
  self.font_name = font_name
  self.font_size = font_size
}

///|
/// Set the optional font pack emitted before subsequent text operations.
pub fn PdfDrawControl::set_fontpack(
  self : PdfDrawControl,
  fontpack : PdfFontPack?,
) -> Unit {
  self.fontpack = fontpack
  self.fontpack_initialized = false
}

///|
/// Return whether the current font pack has been emitted once.
///
/// This exposes the per-instance state corresponding to cpdf's
/// `fontpack_initialised` ref.
pub fn PdfDrawControl::fontpack_initialized(self : PdfDrawControl) -> Bool {
  self.fontpack_initialized
}

///|
/// Set the paragraph indent used by paragraph text helpers.
pub fn PdfDrawControl::set_paragraph_indent(
  self : PdfDrawControl,
  indent : Double?,
) -> Unit {
  self.paragraph_indent = indent
}

///|
/// Return the current paragraph indent used by `add_paras`.
///
/// This is the per-instance counterpart of `cpdfdrawcontrol.ml`'s `getindent`
/// callback.
pub fn PdfDrawControl::paragraph_indent(self : PdfDrawControl) -> Double? {
  self.paragraph_indent
}

///|
fn PdfDrawControl::ensure_text_section(
  self : PdfDrawControl,
  message : String,
) -> Unit raise @core.PdfError {
  if self.depth() < 2 {
    raise SoftError(message)
  }
}

///|
/// Append the current font pack once under `font_name`.
///
/// This ports `cpdfdrawcontrol.ml`'s `add_default_fontpack fontname` helper.
/// Calling `set_fontpack` resets the one-shot guard.
pub fn PdfDrawControl::add_default_fontpack(
  self : PdfDrawControl,
  font_name : String,
) -> Unit raise @core.PdfError {
  if !self.fontpack_initialized {
    match self.fontpack {
      Some(fontpack) => self.add_op(PdfDrawFontPack(font_name, fontpack, []))
      None => ()
    }
    self.fontpack_initialized = true
  }
}

///|
fn PdfDrawControl::add_current_font(
  self : PdfDrawControl,
) -> Unit raise @core.PdfError {
  self.add_op(PdfDrawFont(self.font_name, self.font_size))
}

///|
/// Append literal text inside the current text section.
pub fn PdfDrawControl::add_text(
  self : PdfDrawControl,
  text : String,
) -> Unit raise @core.PdfError {
  self.ensure_text_section("-text must be in a -bt / -et section")
  self.add_default_fontpack(self.font_name)
  self.add_current_font()
  self.add_op(PdfDrawText(text))
}

///|
/// Append special text inside the current text section.
pub fn PdfDrawControl::add_special_text(
  self : PdfDrawControl,
  text : String,
) -> Unit raise @core.PdfError {
  self.ensure_text_section("-stext must be in a -bt / -et section")
  self.add_default_fontpack(self.font_name)
  self.add_current_font()
  self.add_op(PdfDrawSpecialText(text))
}

///|
fn pdf_draw_control_utf8_string(bytes : BytesView) -> String {
  @utf8.decode(bytes) catch {
    _ => @utf8.decode_lossy(bytes)
  }
}

///|
fn pdf_draw_control_parse_para_spec(
  document : PdfDocument,
  spec : String,
) -> (PdfDrawJustification, Double, String) raise @core.PdfError {
  let bytes = @utf8.encode(spec)
  if bytes.length() == 0 {
    raise SoftError("Unknown justification specification")
  }
  let justification = match bytes[0].to_int() {
    76 => PdfDrawLeft
    82 => PdfDrawRight
    67 => PdfDrawCentre
    _ => raise SoftError("Unknown justification specification")
  }
  let mut separator = -1
  for index in 1..= 0 {
        raise SoftError("addjpeg: bad file specification")
      }
      separator = index
    }
  }
  if separator < 0 {
    raise SoftError("addjpeg: bad file specification")
  }
  let width = document.parse_single_number(
    @ascii.decode_lossy(bytes[1:separator]),
  )
  let text = pdf_draw_control_utf8_string(bytes[separator + 1:])
  (justification, width, text)
}

///|
fn pdf_draw_control_split_escaped_newlines(text : String) -> Array[String] {
  let bytes = @utf8.encode(text)
  let parts : Array[String] = Array(capacity=bytes.length())
  let mut start = 0
  let mut index = 0
  while index < bytes.length() {
    if index + 1 < bytes.length() &&
      bytes[index].to_int() == 92 &&
      bytes[index + 1].to_int() == 110 {
      parts.push(pdf_draw_control_utf8_string(bytes[start:index]))
      index = index + 2
      start = index
    } else {
      index = index + 1
    }
  }
  if start < bytes.length() {
    parts.push(pdf_draw_control_utf8_string(bytes[start:]))
  }
  parts
}

///|
/// Append one paragraph text operation parsed from a cpdf paragraph spec.
pub fn PdfDrawControl::add_para(
  self : PdfDrawControl,
  document : PdfDocument,
  spec : String,
) -> Unit raise @core.PdfError {
  self.ensure_text_section("-para must be in a -bt / -et section")
  self.add_default_fontpack(self.font_name)
  self.add_current_font()
  let (justification, width, text) = pdf_draw_control_parse_para_spec(
    document, spec,
  )
  self.add_op(PdfDrawPara(None, justification, width, [text]))
}

///|
/// Append a multi-line paragraph operation parsed from a cpdf paragraph spec.
pub fn PdfDrawControl::add_paras(
  self : PdfDrawControl,
  document : PdfDocument,
  spec : String,
) -> Unit raise @core.PdfError {
  self.ensure_text_section("-paras must be in a -bt / -et section")
  self.add_default_fontpack(self.font_name)
  self.add_current_font()
  let (justification, width, text) = pdf_draw_control_parse_para_spec(
    document, spec,
  )
  self.add_op(
    PdfDrawPara(
      self.paragraph_indent,
      justification,
      width,
      pdf_draw_control_split_escaped_newlines(text),
    ),
  )
}

///|
/// Start a text-section frame.
pub fn PdfDrawControl::begin_text_section(self : PdfDrawControl) -> Unit {
  self.frames.push({ name: pdf_draw_control_text_frame, ops: [], })
}

///|
/// Finish the current text-section frame and append a `PdfDrawTextSection` op.
pub fn PdfDrawControl::end_text_section(
  self : PdfDrawControl,
) -> Unit raise @core.PdfError {
  match self.frames.pop() {
    Some(frame) if frame.name == pdf_draw_control_text_frame =>
      self.add_op(PdfDrawTextSection(pdf_draw_control_copy_ops(frame.ops)))
    Some(frame) => {
      self.frames.push(frame)
      raise SoftError("not in a text section at -et")
    }
    None => raise SoftError("not in a text section at -et")
  }
}