///|
/// Shape a single paragraph using OpenType GSUB/GPOS and Unicode BiDi.
/// Optional weight/axis coordinates select a variable font instance. Static
/// weights must match the loaded face. No synthesis or font fallback is performed.
/// Kerning and standard/contextual ligatures are enabled by default.
pub fn TTFont::shape_text(
  self : TTFont,
  text : String,
  font_size : Double,
  direction~ : TextDirection = Auto,
  language~ : String = "und",
  kerning~ : Bool = true,
  ligatures~ : Bool = true,
  font_weight? : Double,
  axis_values~ : Map[String, Double] = Map([]),
) -> ShapedText raise ShapingError {
  if !font_size.is_finite() || font_size < 0.0 {
    raise InvalidFontSize
  }
  if self.units_per_em <= 0 || self.data.length() < 12 {
    raise InvalidFont
  }
  let variations = axis_values.copy()
  if font_weight is Some(weight) {
    if !weight.is_finite() || weight < 1.0 || weight > 1000.0 {
      raise InvalidFontWeight
    }
    if self.var_axes.any(axis => axis.tag == "wght") {
      if !variations.contains("wght") {
        variations["wght"] = weight
      }
    } else if weight != self.weight_class().to_double() {
      raise UnsupportedFontWeight
    }
  }
  let settings : Array[@swash.Setting[Double]] = []
  for tag, value in variations {
    let mut selected : VarAxis? = None
    let mut index = 0
    for candidate_index, axis in self.var_axes {
      if axis.tag == tag {
        selected = Some(axis)
        index = candidate_index
      }
    }
    guard selected is Some(axis) else { raise InvalidVariationAxis(tag) }
    if !value.is_finite() {
      raise InvalidVariationAxis(tag)
    }
    let clamped = value.max(axis.min_value).min(axis.max_value)
    variations[tag] = clamped
    let packed = (tag[0].to_int().reinterpret_as_uint() << 24) |
      (tag[1].to_int().reinterpret_as_uint() << 16) |
      (tag[2].to_int().reinterpret_as_uint() << 8) |
      tag[3].to_int().reinterpret_as_uint()
    // moon_swash 0.1.15 reads the avar v1 axis count from the reserved field,
    // so its normalize() omits the segment map. Pre-map the coordinate and
    // express it in linear design units for this pinned backend. Keep the
    // original design coordinate in `variations` for outlines and reporting.
    let normalized = normalize_axis_coord(clamped, axis)
    let mapped = if index < self.avar_segments.length() {
      apply_avar(normalized, self.avar_segments[index])
    } else {
      normalized
    }
    let design = if mapped >= 0.0 {
      axis.default_value + mapped * (axis.max_value - axis.default_value)
    } else {
      axis.default_value + mapped * (axis.default_value - axis.min_value)
    }
    settings.push(@swash.Setting::Setting(packed, design))
  }
  for ch in text {
    if ch == '\n' || ch == '\r' || ch == '\u{2028}' || ch == '\u{2029}' {
      raise MultipleParagraphs
    }
  }
  let para = match direction {
    Auto => @bidi.process(text)
    Ltr => @bidi.process(text, direction=@bidi.LTR)
    Rtl => @bidi.process(text, direction=@bidi.RTL)
  }
  let offsets : Array[Int32] = [0]
  let original : Map[Int32, Int32] = Map([])
  let mut offset = 0
  let missing_codepoints : Array[Int32] = []
  for ch in para.chars {
    original[offset] = ch.to_int()
    if self.glyph_index(ch.to_int()) == 0 &&
      !@swash.CharInfo::from_char(ch).is_ignorable() &&
      !missing_codepoints.contains(ch.to_int()) {
      missing_codepoints.push(ch.to_int())
    }
    offset += if ch.to_int() > 0xFFFF { 2 } else { 1 }
    offsets.push(offset)
  }
  let scripts = resolve_shape_scripts(para.chars)
  let logical : Array[LogicalShapeRun] = []
  let run_for_char = Array::make(para.chars.length(), 0)
  let mut start = 0
  while start < para.chars.length() {
    let mut end = start + 1
    while end < para.chars.length() &&
          para.levels[end] == para.levels[start] &&
          scripts[end].to_opentype() == scripts[start].to_opentype() {
      end += 1
    }
    for i in start.. charmap.map(ch.to_uint())))
      shaper.add_cluster(cluster)
    }
    let clusters : Array[ShapeCluster] = []
    shaper.shape_with(cluster => {
      clusters.push({
        text_start: cluster.source().start().reinterpret_as_int(),
        text_end: cluster.source().end().reinterpret_as_int(),
        glyphs: cluster.glyphs().to_owned(),
      })
    })
    let glyph_start = glyphs.length()
    let run_x = pen_x
    // Swash emits logical clusters. Reverse clusters for RTL while retaining
    // the base/mark order and attachment offsets within each cluster.
    for cluster_index in 0.. Array[@swash.Script] {
  let scripts : Array[@swash.Script] = []
  let mut previous = @swash.Common
  for ch in chars {
    let script = @swash.CharInfo::from_char(ch).properties().script()
    let tag = script.to_opentype()
    if tag != @swash.Common.to_opentype() &&
      tag != @swash.Inherited.to_opentype() &&
      tag != @swash.Unknown.to_opentype() {
      previous = script
    }
    scripts.push(previous)
  }
  let mut next = @swash.Common
  for i = scripts.length() - 1; i >= 0; i = i - 1 {
    if scripts[i].to_opentype() == @swash.Common.to_opentype() {
      scripts[i] = next
    } else {
      next = scripts[i]
    }
  }
  scripts
}

///|
fn script_tag_string(script : @swash.Script) -> String {
  let tag = script.to_opentype()
  String::from_array([
    (tag >> 24).reinterpret_as_int().unsafe_to_char(),
    ((tag >> 16) & 255U).reinterpret_as_int().unsafe_to_char(),
    ((tag >> 8) & 255U).reinterpret_as_int().unsafe_to_char(),
    (tag & 255U).reinterpret_as_int().unsafe_to_char(),
  ])
}