///|
/// Explicit font bytes for an offline worker, deduplicated across face aliases.
pub struct FontDataResult {
  data : Json?
  diagnostics : ReadOnlyArray[RasterDiagnostic]
}

///|
fn validate_font_metadata(
  fonts : FontSet,
  limits : RasterLimits,
) -> Unit raise RasterFailure {
  let mut units = 0L
  for face in fonts.faces {
    units += face.family.length().to_int64()
  }
  for family in fonts.fallback_families {
    units += family.length().to_int64()
  }
  guard units <= limits.max_source_units.to_int64() else {
    raise raster_fail(
      "raster/font-limit", "Font family metadata exceeds the source budget",
    )
  }
}

///|
pub fn FontSet::export_data(
  self : FontSet,
  limits~ : RasterLimits = RasterLimits::default(),
) -> FontDataResult {
  try {
    validate_raster_limits(limits)
    validate_font_metadata(self, limits)
    ignore(load_font_set(self, limits))
    let resources : Array[Json] = []
    let indices : Map[String, Int32] = Map([])
    let faces : Array[Json] = []
    for face in self.faces {
      let encoded = @base64.encode(face.data)
      let index = match indices.get(encoded) {
        Some(index) => index
        None => {
          let index = resources.length()
          indices[encoded] = index
          resources.push(Json(encoded))
          index
        }
      }
      faces.push(
        Json({
          "family": Json(face.family),
          "weight": Json(face.weight),
          "italic": Json(face.italic),
          "resource": Json(index),
        }),
      )
    }
    {
      data: Some({
        "version": 1,
        "resources": Json(resources),
        "faces": Json(faces),
        "fallbacks": Json(
          self.fallback_families.iter().map(value => Json(value)).to_array(),
        ),
      }),
      diagnostics: [],
    }
  } catch {
    error => { data: None, diagnostics: [raster_diagnostic(error)], }
  }
}

///|
/// Decode the generated worker font protocol with byte and face budgets, then
/// run the same font validation used by ordinary library callers.
pub fn FontSet::from_export_data(
  data : Json,
  limits~ : RasterLimits = RasterLimits::default(),
) -> FontSubsetResult {
  try {
    validate_raster_limits(limits)
    guard gif_number(gif_field(data, "version")) == 1L else {
      raise raster_fail("raster/font-data", "Unsupported font protocol version")
    }
    let resources : Array[Bytes] = []
    let mut used = 0L
    for value in gif_array(gif_field(data, "resources"), 128) {
      let text = gif_string(value)
      used += text.length().to_int64()
      guard used <= limits.max_source_units.to_int64() * 4L / 3L + 512L else {
        raise raster_fail(
          "raster/font-limit", "Worker font data exceeds its byte budget",
        )
      }
      let bytes = @base64.decode_lossy(text)
      guard @base64.encode(bytes) == text else {
        raise raster_fail(
          "raster/font-data", "Worker font resource is not canonical base64",
        )
      }
      resources.push(bytes)
    }
    let faces : Array[FontFace] = []
    for value in gif_array(gif_field(data, "faces"), 128) {
      let family = gif_string(gif_field(value, "family"))
      let weight = gif_number(gif_field(value, "weight"))
      let italic = gif_boolean(gif_field(value, "italic"))
      let resource = gif_number(gif_field(value, "resource"))
      guard family.length() <= limits.max_source_units &&
        weight >= 1L &&
        weight <= 1000L &&
        resource < resources.length().to_int64() else {
        raise raster_fail("raster/font-data", "Worker font face is invalid")
      }
      faces.push(
        FontFace::new(
          family,
          resources[resource.to_int()],
          weight=weight.to_int(),
          italic~,
        ),
      )
    }
    let fallbacks = gif_array(gif_field(data, "fallbacks"), 128).map(value => {
      gif_string(value)
    })
    let fonts = FontSet::new(faces, fallback_families=fallbacks)
    validate_font_metadata(fonts, limits)
    ignore(load_font_set(fonts, limits))
    { fonts: Some(fonts), diagnostics: [], }
  } catch {
    error => { fonts: None, diagnostics: [raster_diagnostic(error)], }
  }
}