///|
/// The parsed `xl/richData/*` and `xl/metadata.xml` parts needed to
/// resolve modern Excel embedded cell images ("Place in cell"). Mirrors
/// the chain Excelize's `getImageCellRel` walks: a cell's `vm` index
/// selects a value-metadata block, whose record points at a rich value,
/// whose structure names a `_rvRel:LocalImageIdentifier` value, which
/// indexes `richValueRel.xml` to a relationship, which resolves to a
/// media part.
struct RichValueImages {
  /// value-metadata block index (0-based, i.e. vm-1) -> rich value index
  value_metadata : Array[Int]
  /// rich values: each is (structure index, ordered values)
  rich_values : Array[(Int, Array[String])]
  /// rich value structures: each is the ordered list of key names
  structures : Array[Array[String]]
  /// ordered relationship ids referenced by rich values
  rel_ids : Array[String]
  /// relationship id -> media target (from richValueRel.xml.rels)
  rel_targets : Map[String, String]
  /// ordered blip relationship ids of web images, from
  /// rdRichValueWebImage.xml (used by the IMAGE() function)
  web_blip_ids : Array[String]
  /// blip relationship id -> media target (from
  /// rdRichValueWebImage.xml.rels)
  web_rel_targets : Map[String, String]
  /// media target -> manifest-authenticated image identity
  media_identities : Map[String, ImagePartIdentity]
}

///|
/// Reads the rich-value image parts from the archive, returning `None`
/// when the workbook has no rich value metadata.
fn parse_rich_value_images(
  archive : @zip.Archive,
  part_names : Map[String, String],
  content_types : @ooxml.PackageContentTypes,
  decode : (BytesView) -> String raise XlsxError,
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> RichValueImages? raise XlsxError {
  let metadata_bytes = archive.get("xl/metadata.xml")
  let rich_value_bytes = archive.get("xl/richData/rdrichvalue.xml")
  guard metadata_bytes is Some(md) && rich_value_bytes is Some(rv) else {
    return None
  }
  let value_metadata = parse_value_metadata(decode(md))
  let rich_values = parse_rich_values(decode(rv))
  let structures = match archive.get("xl/richData/rdrichvaluestructure.xml") {
    Some(value) => parse_rich_value_structures(decode(value))
    None => []
  }
  let rel_ids = match archive.get("xl/richData/richValueRel.xml") {
    Some(value) => parse_rich_value_rel_ids(decode(value))
    None => []
  }
  let rich_value_rels_path = actual_archive_part_path(
    part_names, "xl/richData/_rels/richValueRel.xml.rels",
  )
  let raw_rel_targets = match
    load_optional_relationship_part(
      archive,
      content_types,
      rich_value_rels_path,
      "rich value relationships",
      cancelled~,
    ) {
    Some(value) =>
      parse_internal_relationship_targets(
        decode(value),
        rel_image,
        budget?,
        cancelled~,
      )
    None => Map([])
  }
  let rel_targets : Map[String, String] = Map([])
  for rel_id, target in raw_rel_targets {
    rel_targets[rel_id] = actual_relationship_target_path(
      "xl/richData/richValueRel.xml",
      target,
      part_names,
      cancelled~,
    )
  }
  let web_blip_ids = match archive.get("xl/richData/rdRichValueWebImage.xml") {
    Some(value) => parse_web_image_blip_ids(decode(value))
    None => []
  }
  let rich_value_web_rels_path = actual_archive_part_path(
    part_names, "xl/richData/_rels/rdRichValueWebImage.xml.rels",
  )
  let raw_web_rel_targets = match
    load_optional_relationship_part(
      archive,
      content_types,
      rich_value_web_rels_path,
      "rich value web image relationships",
      cancelled~,
    ) {
    Some(value) =>
      parse_internal_relationship_targets(
        decode(value),
        rel_image,
        budget?,
        cancelled~,
      )
    None => Map([])
  }
  let web_rel_targets : Map[String, String] = Map([])
  for rel_id, target in raw_web_rel_targets {
    web_rel_targets[rel_id] = actual_relationship_target_path(
      "xl/richData/rdRichValueWebImage.xml",
      target,
      part_names,
      cancelled~,
    )
  }
  Some({
    value_metadata,
    rich_values,
    structures,
    rel_ids,
    rel_targets,
    web_blip_ids,
    web_rel_targets,
    media_identities: Map([]),
  })
}

///|
test "rich value image relationships use source-relative OPC resolution" {
  let archive = @zip.Archive::new()
  archive.add("xl/metadata.xml", b"")
  archive.add("xl/richData/rdrichvalue.xml", b"")
  archive.add(
    "xl/richData/_rels/richValueRel.xml.rels",
    @encoding/utf8.encode(
      (
        #|
      ),
    ),
  )
  archive.add("decoy.png", b"root image")
  archive.add("xl/decoy.png", b"wrong image")
  let parsed = parse_rich_value_images(
    archive,
    archive_part_name_index(archive),
    image_reader_test_content_types(),
    value => {
      @encoding/utf8.decode(value) catch {
        _ => raise InvalidXml(msg="invalid utf8")
      }
    },
  )
  guard parsed is Some(data) else { fail("expected rich value image data") }
  assert_eq(data.rel_targets.get("rId1"), Some("decoy.png"))
}

///|
/// Parses the ordered blip relationship ids from ``
/// elements of `xl/richData/rdRichValueWebImage.xml`. Each web image's
/// `` names the relationship resolving to its media.
fn parse_web_image_blip_ids(xml : String) -> Array[String] raise XlsxError {
  let result : Array[String] = []
  let mut first = true
  for chunk in xml.split("") {
      Some(pos) => pos
      None => text.length()
    }
    let srd_body = text[:end]
    let blip_attrs = match tag_attributes_in(srd_body, "blip") {
      Some(value) => value
      None => continue
    }
    let id = match attr_value(blip_attrs, "r:id") {
      Some(value) => Some(value)
      None => attr_value(blip_attrs, "id")
    }
    match id {
      Some(value) => result.push(value)
      None => continue
    }
  }
  result
}

///|
/// Parses the `` blocks of `xl/metadata.xml`, returning
/// the first record value of each block (the rich value index).
fn parse_value_metadata(xml : String) -> Array[Int] raise XlsxError {
  let result : Array[Int] = []
  let body = match extract_tag_body_from(xml, "valueMetadata") {
    Some(value) => value
    None => return result
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => text.length()
    }
    let bk_body = text[:end]
    match tag_attributes_in(bk_body, "rc") {
      Some(attrs) =>
        match attr_value(attrs, "v") {
          Some(v) =>
            result.push(@string.parse_int(v, base=10) catch { _ => -1 })
          None => result.push(-1)
        }
      None => result.push(-1)
    }
  }
  result
}

///|
/// Parses `` rich values from `xl/richData/rdrichvalue.xml`: each is
/// its structure index `s` and the ordered `` values.
fn parse_rich_values(
  xml : String,
) -> Array[(Int, Array[String])] raise XlsxError {
  let result : Array[(Int, Array[String])] = []
  // Extract the rvData body first so the split does not also match the
  // " value
    None => return result
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => continue
    }
    let tag = text[:tag_end]
    let s = match attr_value(tag, "s") {
      Some(value) => @string.parse_int(value, base=10) catch { _ => 0 }
      None => 0
    }
    let end = match text.find("") {
      Some(pos) => pos
      None => text.length()
    }
    let rv_body = text[tag_end + 1:end]
    let values : Array[String] = []
    let mut first_v = true
    for v_chunk in rv_body.split("") {
      if first_v {
        first_v = false
        continue
      }
      let v_text = v_chunk.to_owned()
      match v_text.find("") {
        Some(pos) => values.push(unescape_xml_text(v_text[:pos]))
        None => ()
      }
    }
    result.push((s, values))
  }
  result
}

///|
/// Parses `` structures from `xl/richData/rdrichvaluestructure.xml`:
/// each is the ordered list of `` key names.
fn parse_rich_value_structures(
  xml : String,
) -> Array[Array[String]] raise XlsxError {
  let result : Array[Array[String]] = []
  let mut first = true
  for chunk in xml.split("") {
      Some(pos) => pos
      None => text.length()
    }
    let s_body = text[:end]
    let keys : Array[String] = []
    let mut first_k = true
    for k_chunk in s_body.split("
          match attr_value(attrs, "n") {
            Some(n) => keys.push(unescape_xml_text(n))
            None => keys.push("")
          }
        None => keys.push("")
      }
    }
    result.push(keys)
  }
  result
}

///|
/// Parses the ordered relationship ids from `` elements of
/// `xl/richData/richValueRel.xml`.
fn parse_rich_value_rel_ids(xml : String) -> Array[String] raise XlsxError {
  let result : Array[String] = []
  let mut first = true
  for chunk in xml.split("") {
      Some(pos) => pos
      None => continue
    }
    let tag = text[:end]
    let id = match attr_value(tag, "r:id") {
      Some(value) => Some(value)
      None => attr_value(tag, "id")
    }
    match id {
      Some(value) => result.push(value)
      None => ()
    }
  }
  result
}

///|
/// Scans a worksheet's cells for the `vm` (value-metadata) attribute,
/// returning a canonical-reference -> vm-index map. Rich-value cell
/// images are the only cells that carry `vm`, so this is the link
/// between a cell and its rich value.
fn parse_cell_vm_map(sheet_xml : String) -> Map[String, Int] raise XlsxError {
  let result : Map[String, Int] = Map([])
  let mut first = true
  for chunk in sheet_xml.split("") {
      Some(pos) => pos
      None => continue
    }
    let tag = text[:end]
    let vm = match attr_value(tag, "vm") {
      Some(value) => value
      None => continue
    }
    let reference = match attr_value(tag, "r") {
      Some(value) => value
      None => continue
    }
    let vm_index = @string.parse_int(vm, base=10) catch { _ => continue }
    let (row, col) = cell_ref_to_rc(reference)
    result[cell_ref_from(row, col)] = vm_index
  }
  result
}

///|
/// Resolves a rich value's local ("Place in cell") image media target
/// via the `_rvRel:LocalImageIdentifier` key, or `None` when the value
/// is not a local image.
fn rich_value_local_target(
  data : RichValueImages,
  keys : Array[String],
  values : Array[String],
) -> String? {
  let local_idx = rich_value_key_index(keys, "_rvRel:LocalImageIdentifier")
  if local_idx < 0 || local_idx >= values.length() {
    return None
  }
  let rel_index = @string.parse_int(values[local_idx], base=10) catch {
    _ => return None
  }
  if rel_index < 0 || rel_index >= data.rel_ids.length() {
    return None
  }
  data.rel_targets.get(data.rel_ids[rel_index])
}

///|
/// Resolves a rich value's web (IMAGE function) image media target via
/// the `WebImageIdentifier` key, or `None` when the value is not a web
/// image. Mirrors Excelize's getRichDataWebImagesRel.
fn rich_value_web_target(
  data : RichValueImages,
  keys : Array[String],
  values : Array[String],
) -> String? {
  let web_idx = rich_value_key_index(keys, "WebImageIdentifier")
  if web_idx < 0 || web_idx >= values.length() {
    return None
  }
  let web_index = @string.parse_int(values[web_idx], base=10) catch {
    _ => return None
  }
  if web_index < 0 || web_index >= data.web_blip_ids.length() {
    return None
  }
  data.web_rel_targets.get(data.web_blip_ids[web_index])
}

///|
/// Returns the index of a key name within a rich value structure, or -1.
fn rich_value_key_index(keys : Array[String], name : String) -> Int {
  for i, key in keys {
    if key == name {
      return i
    }
  }
  -1
}

///|
/// Resolves the modern "Place in cell" embedded image shown in
/// `reference`, mirroring Excelize's `getImageCellRel`
/// (`_rvRel:LocalImageIdentifier` path). Returns `None` when the cell has
/// no rich-value image.
fn Workbook::rich_value_image_for_cell(
  self : Workbook,
  sheet : Worksheet,
  reference : StringView,
) -> Image? raise XlsxError {
  let data = match self.rich_value_images {
    Some(value) => value
    None => return None
  }
  let canonical = {
    let (row, col) = cell_ref_to_rc(reference)
    cell_ref_from(row, col)
  }
  let vm = match sheet.cell_vm.get(canonical) {
    Some(value) => value
    None => return None
  }
  // Only #VALUE! cells carry an embedded rich-value image.
  if sheet.get_cell(canonical) != Some(formula_error_value) {
    return None
  }
  if vm < 1 || vm > data.value_metadata.length() {
    return None
  }
  let rich_value_idx = data.value_metadata[vm - 1]
  if rich_value_idx < 0 || rich_value_idx >= data.rich_values.length() {
    return None
  }
  let (structure_idx, values) = data.rich_values[rich_value_idx]
  if structure_idx < 0 || structure_idx >= data.structures.length() {
    return None
  }
  let keys = data.structures[structure_idx]
  // Excelize requires the structure keys and rich value values to line
  // up before reading any of them (getImageCellRel:
  // len(rvStruct.K) != len(rv.V)); a mismatched rich value is not an
  // image.
  if keys.length() != values.length() {
    return None
  }
  let alt_text = {
    let text_idx = rich_value_key_index(keys, "Text")
    if text_idx >= 0 && text_idx < values.length() {
      values[text_idx]
    } else {
      ""
    }
  }
  // A rich value carries either a local image (Place in cell) or a web
  // image (IMAGE function). Excelize's getImageCellRel branches on which
  // key is *present*: once a LocalImageIdentifier key exists it commits
  // to the local path and never falls back to the web path, even if the
  // local relationship fails to resolve.
  let target = if rich_value_key_index(keys, "_rvRel:LocalImageIdentifier") !=
    -1 {
    rich_value_local_target(data, keys, values)
  } else {
    rich_value_web_target(data, keys, values)
  }
  let media_path = match target {
    Some(value) => value
    None => return None
  }
  match self.rich_value_media.get(media_path) {
    Some(bytes) => {
      let identity = match data.media_identities.get(media_path) {
        Some(value) => value
        None => return None
      }
      Some({
        reference: canonical,
        data: bytes,
        extension: "." + identity.extension,
        content_type: identity.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: "",
        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,
      })
    }
    None => None
  }
}

///|
test "parse_rich_values does not create a phantom entry from rvData" {
  let xml =
    #|
    #|
    #|  0alt one
    #|  2
    #|
  let values = parse_rich_values(xml)
  // exactly two rich values, index 0 is the first (not a phantom)
  inspect(values.length(), content="2")
  let (s0, v0) = values[0]
  inspect(s0, content="0")
  debug_inspect(
    v0,
    content=(
      #|["0", "alt one"]
    ),
  )
  let (s1, v1) = values[1]
  inspect(s1, content="1")
  debug_inspect(
    v1,
    content=(
      #|["2"]
    ),
  )
}

///|
test "parse_value_metadata reads block record values" {
  let xml =
    #|
    #|  
    #|    
    #|    
    #|  
    #|
  debug_inspect(
    parse_value_metadata(xml),
    content=(
      #|[0, 3]
    ),
  )
}

///|
test "parse_rich_value_structures extracts ordered key names" {
  let xml =
    #|
    #|  
    #|    
    #|    
    #|  
    #|
  let structures = parse_rich_value_structures(xml)
  inspect(structures.length(), content="1")
  debug_inspect(
    structures[0],
    content=(
      #|["_rvRel:LocalImageIdentifier", "Text"]
    ),
  )
}

///|
test "parse_rich_value_rel_ids reads ordered relationship ids" {
  let xml =
    #|
    #|  
    #|  
    #|
  debug_inspect(
    parse_rich_value_rel_ids(xml),
    content=(
      #|["rId1", "rId2"]
    ),
  )
}

///|
test "parse_web_image_blip_ids reads ordered blip relationship ids" {
  let xml =
    #|
    #|  
#| #| debug_inspect( parse_web_image_blip_ids(xml), content=( #|["rId5", "rId6"] ), ) }