///|
/// A Kingsoft WPS Office embedded cell image, extracted from
/// `xl/cellimages.xml` and its relationships. The `name` is the image
/// identifier a `DISPIMG` formula references; `data` is the raw media,
/// `extension` its file extension (with the leading dot, e.g. ".png"),
/// and `alt_text` the descriptive text from `cNvPr@descr`.
struct CellImage {
  name : String
  data : Bytes
  extension : String
  content_type : String
  alt_text : String
}

///|
/// Parses `xl/cellimages.xml` plus its relationships into the list of
/// embedded cell images, loading each referenced media part from the
/// archive. Mirrors Excelize's `cellImagesReader` / `getDispImages`.
/// Images whose media part is absent are skipped, like Go.
fn parse_cell_images(
  cell_images_xml : StringView,
  rels_xml : StringView,
  source_part : StringView,
  part_names : Map[String, String],
  content_types : @ooxml.PackageContentTypes,
  archive : @zip.Archive,
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> Array[CellImage] raise XlsxError {
  let images : Array[CellImage] = []
  let targets = if rels_xml is "" {
    Map([])
  } else {
    // cellimages relationships are always image relationships
    parse_internal_relationship_targets(
      rels_xml,
      rel_image,
      budget?,
      cancelled~,
    )
  }
  let xml_str = cell_images_xml.to_owned()
  // Each embedded image is one <...pic> element; cellimages.xml contains
  // only cell images, so every pic here is one. The WPS namespace prefix
  // (xdr:) is matched, falling back to an unprefixed .
  let (open_tag, close_tag) = if xml_str.contains("")
  } else {
    ("")
  }
  let mut first = true
  for chunk in xml_str.split(open_tag) {
    if first {
      first = false
      continue
    }
    let text = chunk.to_owned()
    let end = match text.find(close_tag) {
      Some(pos) => pos
      None => continue
    }
    let pic_body = text[:end]
    let cnv_attrs = match tag_attributes_in(pic_body, "xdr:cNvPr") {
      Some(value) => value
      None =>
        match tag_attributes_in(pic_body, "cNvPr") {
          Some(value) => value
          None => continue
        }
    }
    let name = match attr_value(cnv_attrs, "name") {
      Some(value) => value
      None => continue
    }
    let alt_text = match attr_value(cnv_attrs, "descr") {
      Some(value) => value
      None => ""
    }
    // Narrow to the blipFill body first: an unprefixed ``, which carries no r:embed.
    let blip_fill_body = match extract_tag_body_from(pic_body, "xdr:blipFill") {
      Some(value) => value
      None =>
        match extract_tag_body_from(pic_body, "blipFill") {
          Some(value) => value
          None => continue
        }
    }
    let blip_attrs = match tag_attributes_in(blip_fill_body, "a:blip") {
      Some(value) => value
      None =>
        match tag_attributes_in(blip_fill_body, "blip") {
          Some(value) => value
          None => continue
        }
    }
    let embed = match attr_value(blip_attrs, "r:embed") {
      Some(value) => value
      None =>
        match attr_value(blip_attrs, "embed") {
          Some(value) => value
          None => continue
        }
    }
    let target = match targets.get(embed) {
      Some(value) => value
      None => continue
    }
    let media_path = actual_relationship_target_path(
      source_part,
      target,
      part_names,
      cancelled~,
    )
    match archive.get(media_path) {
      Some(_) => {
        let (data, identity) = load_image_part(
          archive,
          content_types,
          media_path,
          "cell image",
          cancelled~,
        )
        images.push({
          name: unescape_xml_text(name),
          data: data.to_owned(),
          extension: "." + identity.extension,
          content_type: identity.content_type,
          alt_text: unescape_xml_text(alt_text),
        })
      }
      None => continue
    }
  }
  images
}

///|
/// Builds an `Image` positioned in `reference` from an embedded cell
/// image. Cell images have no anchor geometry, so offsets/size are zero
/// and positioning is one-cell, matching how Excelize surfaces a
/// place-in-cell `Picture`.
fn CellImage::to_image(self : CellImage, reference : String) -> Image {
  {
    reference,
    data: self.data,
    extension: self.extension,
    content_type: self.content_type,
    offset_x: 0,
    offset_y: 0,
    scale_x: 1.0,
    scale_y: 1.0,
    width_emu: 0,
    height_emu: 0,
    print_object: true,
    locked: false,
    hyperlink: "",
    hyperlink_type: Unset,
    name: self.name,
    alt_text: self.alt_text,
    lock_aspect_ratio: false,
    positioning: OneCell,
    drawing_offset_x_emu: None,
    drawing_offset_y_emu: None,
    drawing_width_emu: None,
    drawing_height_emu: None,
    drawing_order: None,
  }
}

///|
/// Resolves the Kingsoft WPS Office embedded images (`DISPIMG`) shown in
/// `reference`, mirroring Excelize's `getDispImages`: a cell whose
/// formula is a `DISPIMG` call evaluates to the image identifier, which
/// selects the matching `cellimages.xml` entry.
fn Workbook::disp_images_for_cell(
  self : Workbook,
  sheet : Worksheet,
  reference : StringView,
) -> Array[Image] raise XlsxError {
  let result : Array[Image] = []
  if self.cell_images.length() == 0 {
    return result
  }
  let formula = match sheet.get_cell_formula(reference) {
    Some(value) => value
    None => return result
  }
  if !is_disp_img_formula(formula) {
    return result
  }
  let img_id = self.calc_cell_value(sheet.name(), reference)
  let canonical = {
    let (row, col) = cell_ref_to_rc(reference)
    cell_ref_from(row, col)
  }
  for image in self.cell_images {
    if image.name == img_id {
      result.push(image.to_image(canonical))
    }
  }
  result
}

///|
/// Reports whether a formula is a `DISPIMG` call, tolerating a leading
/// `=` and the `_xlfn.` future-function prefix, like Excelize.
fn is_disp_img_formula(formula : String) -> Bool {
  let mut text = formula
  if text.has_prefix("=") {
    text = text[1:].to_owned()
  }
  if text.has_prefix("_xlfn.") {
    text = text[6:].to_owned()
  }
  text.has_prefix("DISPIMG")
}

///|
test "parse_cell_images handles unprefixed blip without matching blipFill" {
  // an unprefixed cellimages.xml:  must not be mistaken for 
  let cellimages_xml =
    #|
    #|  
    #|    
    #|      
    #|      
    #|    
    #|  
    #|
  let rels_xml =
    #|
    #|  
    #|
  let archive = @zip.Archive::new()
  archive.add("xl/media/u.png", b"\x89PNGunprefixed")
  let images = parse_cell_images(
    cellimages_xml,
    rels_xml,
    "xl/cellimages.xml",
    archive_part_name_index(archive),
    image_reader_test_content_types(),
    archive,
  )
  inspect(images.length(), content="1")
  inspect(images[0].name, content="ID_U")
  inspect(images[0].extension, content=".png")
}

///|
test "parse_cell_images resolves targets relative to its source part" {
  let cellimages_xml =
    #|
  let rels_xml =
    #|
  let archive = @zip.Archive::new()
  archive.add("decoy.png", b"root image")
  archive.add("xl/decoy.png", b"wrong image")
  let images = parse_cell_images(
    cellimages_xml,
    rels_xml,
    "xl/cellimages.xml",
    archive_part_name_index(archive),
    image_reader_test_content_types(),
    archive,
  )
  assert_eq(images.length(), 1)
  assert_eq(images[0].data, b"root image")
}