///|
fn PdfDrawRenderState::text_ops(
  self : PdfDrawRenderState,
  text : String,
) -> Array[@content.PdfContentOp] raise @core.PdfError {
  let width_cache = pdf_draw_font_width_cache_new(self.font_size)
  let (ops, _) = self.text_ops_and_width_with_cache(text, width_cache)
  ops
}

///|
priv struct PdfDrawFontWidthCache {
  font_size : Double
  entries : Array[(Int, Array[Double])]
}

///|
fn pdf_draw_font_width_cache_new(font_size : Double) -> PdfDrawFontWidthCache {
  { font_size, entries: [], }
}

///|
fn PdfDrawFontWidthCache::widths(
  self : PdfDrawFontWidthCache,
  font_index : Int,
  font : PdfFont,
) -> Array[Double] raise @core.PdfError {
  for entry in self.entries {
    if entry.0 == font_index {
      return entry.1
    }
  }
  let widths = pdf_type_font_widths(font, self.font_size)
  self.entries.push((font_index, widths))
  widths
}

///|
fn PdfDrawRenderState::text_ops_and_width_with_cache(
  self : PdfDrawRenderState,
  text : String,
  width_cache : PdfDrawFontWidthCache,
) -> (Array[@content.PdfContentOp], Double) raise @core.PdfError {
  if self.current_fontpack_identifier == "" {
    raise SoftError("FontPack not selected")
  }
  let codepoints = pdf_codepoints_of_utf8(@utf8.encode(text))
  let triples : Array[(Int, Int, PdfFont)] = Array(capacity=codepoints.length())
  let mut width = 0.0
  for codepoint in codepoints {
    match self.current_fontpack.get_char(codepoint) {
      Some((charcode, font_index, font)) => {
        let widths = width_cache.widths(font_index, font)
        if charcode < 0 || charcode >= widths.length() {
          raise FontWidthExpected
        }
        width += widths[charcode]
        triples.push((charcode, font_index, font))
      }
      None => ()
    }
  }
  let output : Array[@content.PdfContentOp] = Array(
    capacity=triples.length() * 2,
  )
  for run in pdf_fontpack_collate_runs(triples) {
    guard run.length() > 0 else { continue }
    let font_index = run[0].1
    let resource_name = self.font_resource_name(
      self.current_fontpack_identifier,
      font_index,
    )
    let bytes = Array::make(run.length(), b'\x00')
    for index, triple in run {
      bytes[index] = @core.pdf_byte_of_int(triple.0)
    }
    output.push(Op_Tf(resource_name, self.font_size))
    output.push(Op_Tj(Bytes::from_array(bytes)))
  }
  (output, width)
}

///|
fn pdf_draw_decode_lossy_view(bytes : BytesView) -> String {
  let output = Array::make(bytes.length(), b'\x00')
  for index, byte in bytes {
    output[index] = byte
  }
  @utf8.decode_lossy(Bytes::from_array(output))
}

///|
fn pdf_draw_split_on_space(text : String) -> Array[String] {
  let bytes = @utf8.encode(text)
  let parts : Array[String] = Array(capacity=bytes.length())
  let mut part_start = 0
  let mut index = 0
  while index < bytes.length() {
    if bytes[index] == b' ' {
      parts.push(pdf_draw_decode_lossy_view(bytes[part_start:index]))
      index += 1
      part_start = index
    } else {
      index += 1
    }
  }
  parts.push(pdf_draw_decode_lossy_view(bytes[part_start:]))
  parts
}

///|
fn pdf_draw_remove_duplicate_tfs(
  ops : ArrayView[@content.PdfContentOp],
) -> Array[@content.PdfContentOp] {
  let output : Array[@content.PdfContentOp] = Array(capacity=ops.length())
  let mut previous : @core.PdfName? = None
  for op in ops {
    match op {
      Op_Tf(font, _) =>
        match previous {
          Some(seen) if seen == font => ()
          _ => {
            output.push(op)
            previous = Some(font)
          }
        }
      _ => output.push(op)
    }
  }
  output
}

///|
fn pdf_draw_merge_adjacent_tjs(
  ops : ArrayView[@content.PdfContentOp],
) -> Array[@content.PdfContentOp] {
  let output : Array[@content.PdfContentOp] = Array(capacity=ops.length())
  let mut index = 0
  while index < ops.length() {
    match ops[index] {
      Op_Tj(_) => {
        let run_start = index
        let mut total_length = 0
        while index < ops.length() {
          match ops[index] {
            Op_Tj(text) => {
              total_length += text.length()
              index += 1
            }
            _ => break
          }
        }
        let bytes = Array::make(total_length, b'\x00')
        let mut position = 0
        for op_index in run_start..
              for byte in text {
                bytes[position] = byte
                position += 1
              }
            _ => ()
          }
        }
        output.push(Op_Tj(Bytes::from_array(bytes)))
      }
      _ => {
        output.push(ops[index])
        index += 1
      }
    }
  }
  output
}

///|
fn pdf_draw_clean_up_text_ops(
  ops : ArrayView[@content.PdfContentOp],
) -> Array[@content.PdfContentOp] {
  pdf_draw_merge_adjacent_tjs(pdf_draw_remove_duplicate_tfs(ops))
}

///|
fn PdfDrawRenderState::justify_paragraph_line(
  self : PdfDrawRenderState,
  ops : ArrayView[@content.PdfContentOp],
  first_line : Bool,
  indent : Double,
  justification : PdfDrawJustification,
  remaining : Double,
) -> Array[@content.PdfContentOp] {
  ignore(self)
  let output : Array[@content.PdfContentOp] = Array(capacity=ops.length() + 2)
  match justification {
    PdfDrawLeft => {
      if first_line {
        output.push(Op_Td(-indent, 0.0))
      }
      output.append(ops)
      if first_line {
        output.push(Op_Td(indent, 0.0))
      }
    }
    PdfDrawRight => {
      output.push(Op_Td(-remaining, 0.0))
      output.append(ops)
      output.push(Op_Td(remaining, 0.0))
    }
    PdfDrawCentre => {
      output.push(Op_Td(-remaining / 2.0, 0.0))
      output.append(ops)
      output.push(Op_Td(remaining / 2.0, 0.0))
    }
  }
  output
}

///|
fn PdfDrawRenderState::format_paragraph(
  self : PdfDrawRenderState,
  indent : Double,
  justification : PdfDrawJustification,
  width : Double,
  text : String,
) -> Array[@content.PdfContentOp] raise @core.PdfError {
  let width_cache = pdf_draw_font_width_cache_new(self.font_size)
  let split_words = pdf_draw_split_on_space(text)
  let words : Array[(Array[@content.PdfContentOp], Double)] = Array(
    capacity=split_words.length(),
  )
  for word in split_words {
    words.push(self.text_ops_and_width_with_cache(word, width_cache))
  }
  let (space_ops, space_width) = self.text_ops_and_width_with_cache(
    " ", width_cache,
  )
  let output : Array[@content.PdfContentOp] = Array(capacity=words.length() * 2)
  let mut line_ops : Array[@content.PdfContentOp] = Array(
    capacity=words.length() * 2,
  )
  let mut remaining = width
  let mut first_line = true
  let mut first_loop = true
  let mut index = 0
  while index < words.length() {
    if first_loop {
      remaining -= indent
      first_loop = false
    }
    let (word_ops, word_width) = words[index]
    if remaining == width {
      line_ops.append(word_ops)
      remaining -= word_width
      index += 1
    } else if word_width + space_width <= remaining {
      line_ops.append(space_ops)
      line_ops.append(word_ops)
      remaining -= word_width + space_width
      index += 1
    } else {
      output.push(Op_Tstar)
      output.append(
        self.justify_paragraph_line(
          line_ops, first_line, indent, justification, remaining,
        ),
      )
      first_line = false
      line_ops = []
      remaining = width
    }
  }
  output.push(Op_Tstar)
  output.append(
    self.justify_paragraph_line(
      line_ops, first_line, indent, justification, remaining,
    ),
  )
  pdf_draw_clean_up_text_ops(output)
}

///|
fn PdfDrawRenderState::paragraph_ops(
  self : PdfDrawRenderState,
  indent : Double?,
  justification : PdfDrawJustification,
  width : Double,
  paragraphs : ArrayView[String],
) -> Array[@content.PdfContentOp] raise @core.PdfError {
  let output : Array[@content.PdfContentOp] = Array(
    capacity=paragraphs.length(),
  )
  let mut first = true
  for paragraph in paragraphs {
    if !first {
      match indent {
        None => output.push(Op_Tstar)
        Some(_) => ()
      }
    }
    let paragraph_indent = match indent {
      Some(value) if !first => value
      _ => 0.0
    }
    let paragraph_ops = self.format_paragraph(
      paragraph_indent, justification, width, paragraph,
    )
    if self.structure_tree_enabled && self.auto_tag {
      output.append(self.structure_tagged_ops("/P", paragraph_ops))
    } else {
      output.append(paragraph_ops)
    }
    first = false
  }
  output
}