///|
fn stable_kitty_id(seed : String) -> Int {
  let mut hash = 5381
  let modulus = 16777213
  for c in seed {
    hash = (hash * 131 + c.to_int()) % modulus
  }
  (hash + 1).max(1)
}

///|
fn kitty_image_id_for_src(src : String) -> Int {
  stable_kitty_id("image:" + src)
}

///|
fn kitty_placement_id_for_source(source_id : String) -> Int {
  stable_kitty_id("placement:" + source_id)
}

///|
fn current_kitty_placement(region : @tui.ImageRegion) -> KittyPlacement {
  let image_id = kitty_image_id_for_src(region.src)
  let placement_id = kitty_placement_id_for_source(region.source_id)
  {
    image_id,
    placement_id,
    col: region.col,
    row: region.row,
    width: region.width,
    height: region.height,
  }
}

///|
fn same_kitty_placement(lhs : KittyPlacement, rhs : KittyPlacement) -> Bool {
  lhs.image_id == rhs.image_id &&
  lhs.placement_id == rhs.placement_id &&
  lhs.col == rhs.col &&
  lhs.row == rhs.row &&
  lhs.width == rhs.width &&
  lhs.height == rhs.height
}

///|
/// Build kitty graphics overlay commands for image regions
fn Browser::build_kitty_image_overlay(self : Browser) -> String {
  let next_visible : Map[String, KittyPlacement] = {}
  let render_specs : Array[KittyRenderSpec] = []
  for region in self.image_regions {
    match self.image_cache.get(region.src) {
      Some(cached_data) => {
        let placement = current_kitty_placement(region)
        next_visible.set(region.source_id, placement)
        render_specs.push({ region, cached_data, placement })
      }
      None => ()
    }
  }
  if next_visible.is_empty() && self.kitty_visible_placements.is_empty() {
    return ""
  }
  let buf = StringBuilder::new()
  for source_id, previous in self.kitty_visible_placements {
    match next_visible.get(source_id) {
      Some(current) if current.image_id == previous.image_id => ()
      _ =>
        buf.write_string(
          @painter_terminal.encode_kitty_delete_placement(
            previous.image_id,
            previous.placement_id,
          ),
        )
    }
  }
  for spec in render_specs {
    let placement_key = spec.region.source_id
    let uploaded = self.kitty_uploaded_image_ids.contains(
      spec.placement.image_id,
    )
    let previous = self.kitty_visible_placements.get(placement_key)
    let unchanged = match previous {
      Some(prev) => same_kitty_placement(prev, spec.placement)
      None => false
    }
    if uploaded && unchanged {
      continue
    }
    buf.write_string(
      "\u001b[" +
      (spec.region.row + 1).to_string() +
      ";" +
      (spec.region.col + 1).to_string() +
      "H",
    )
    let kitty_data = if uploaded {
      @painter_terminal.encode_kitty_put_placement(
        spec.placement.image_id,
        spec.placement.placement_id,
        spec.placement.width,
        spec.placement.height,
      )
    } else {
      render_cached_kitty_image_with_placement(
        spec.cached_data,
        spec.region,
        spec.placement.image_id,
        spec.placement.placement_id,
      )
    }
    if kitty_data.length() > 0 {
      buf.write_string(kitty_data)
      self.kitty_uploaded_image_ids.set(spec.placement.image_id, true)
    }
  }
  self.kitty_visible_placements = next_visible
  buf.to_string()
}

///|
fn Browser::cache_image_data(
  self : Browser,
  src : String,
  data : String,
) -> Unit {
  self.image_cache.put(src, data)
}

///|
/// Prefetch images from HTML and cache rendered kitty graphics data
async fn Browser::prefetch_images(self : Browser) -> Unit {
  if self.html_content.length() == 0 {
    return
  }
  let srcs = extract_img_srcs(self.html_content)
  for src in srcs {
    if self.image_cache.contains(src) {
      continue
    }
    if self.cache_data_image_src(src) {
      continue
    }
    if src.to_lower().has_prefix("data:") {
      continue
    }
    let abs_url = resolve_url(self.current_url, src)
    if abs_url.length() == 0 {
      continue
    }
    let body = try {
      let options = {
        ..@http.FetchOptions::default(),
        sandbox: self.request_sandbox,
      }
      let response = @http_cache.cached_fetch_async(
        url=abs_url,
        options~,
        cache=self.profile.http_cache(),
        fetcher=http_fetch_adapter,
      )
      response.body
    } catch {
      _ => continue
    }
    if body.length() == 0 {
      continue
    }
    let is_svg = abs_url.to_lower().has_suffix(".svg") ||
      abs_url.to_lower().contains(".svg?")
    if is_svg {
      self.cache_image_data(src, "svg:" + body)
    } else if body.has_prefix("base64:") {
      let b64 = body.unsafe_substring(start=7, end=body.length())
      self.cache_image_data(
        src,
        @terminal_image_cache.prepare_raster_image_for_kitty_cache(b64),
      )
    }
  }
}

///|
fn Browser::cache_data_image_src(self : Browser, src : String) -> Bool {
  match @terminal_image_cache.data_image_src_to_kitty_cache(src) {
    Some(cached_data) => {
      self.cache_image_data(src, cached_data)
      true
    }
    None => false
  }
}

///|
fn rgba_cache_entry_to_resolved_image(
  entry : @terminal_image_cache.RgbaCacheEntry,
) -> @raster.ResolvedImage? {
  match entry.decode_rgba_bytes() {
    Some(rgba) =>
      if rgba.length() >= entry.width * entry.height * 4 {
        Some(
          @raster.ResolvedImage::Raster({
            width: entry.width,
            height: entry.height,
            rgba,
          }),
        )
      } else {
        None
      }
    None => None
  }
}

///|
fn raster_base64_to_resolved_image(
  image_format : String,
  b64_data : String,
) -> @raster.ResolvedImage? {
  match
    @terminal_image_cache.decode_raster_image_to_rgba_cache_entry(
      image_format, b64_data,
    ) {
    Some(entry) => rgba_cache_entry_to_resolved_image(entry)
    None => None
  }
}

///|
fn cached_image_data_to_resolved_image(
  cached_data : String,
) -> @raster.ResolvedImage? {
  match @terminal_image_cache.parse_terminal_image_cache_entry(cached_data) {
    Some(@terminal_image_cache.TerminalImageCacheEntry::Svg(svg_text)) =>
      Some(@raster.ResolvedImage::Svg(svg_text))
    Some(@terminal_image_cache.TerminalImageCacheEntry::RgbaBase64(entry)) =>
      rgba_cache_entry_to_resolved_image(entry)
    Some(@terminal_image_cache.TerminalImageCacheEntry::PngBase64(b64_data)) =>
      raster_base64_to_resolved_image("png", b64_data)
    Some(@terminal_image_cache.TerminalImageCacheEntry::RawBase64(b64_data)) =>
      raster_base64_to_resolved_image(
        @terminal_image_cache.detect_raster_image_format_from_base64(b64_data),
        b64_data,
      )
    None => None
  }
}

///|
fn Browser::install_sixel_image_provider(self : Browser) -> Bool {
  if self.image_cache.is_empty() {
    return false
  }
  @raster.set_image_provider({
    resolve: fn(src) {
      match self.image_cache.get(src) {
        Some(cached_data) => cached_image_data_to_resolved_image(cached_data)
        None => None
      }
    },
  })
  true
}

///|
/// Extract img src attributes from HTML (lightweight, no full parsing)
fn extract_img_srcs(html : String) -> Array[String] {
  @html_assets.extract_img_srcs(html)
}

///|
fn render_cached_kitty_image_with_placement(
  cached_data : String,
  region : @tui.ImageRegion,
  image_id : Int,
  placement_id : Int,
) -> String {
  match @terminal_image_cache.parse_terminal_image_cache_entry(cached_data) {
    Some(@terminal_image_cache.TerminalImageCacheEntry::Svg(svg_text)) => {
      let render_w = region.width * 16
      let render_h = region.height * 32
      @painter_terminal.render_svg_text_to_kitty_cells(
        svg_text,
        render_w,
        render_h,
        region.width,
        region.height,
        image_id~,
        placement_id~,
      )
    }
    Some(@terminal_image_cache.TerminalImageCacheEntry::PngBase64(b64_data)) =>
      @painter_terminal.encode_png_base64_to_kitty_image(
        b64_data,
        region.width,
        region.height,
        image_id~,
        placement_id~,
      )
    Some(@terminal_image_cache.TerminalImageCacheEntry::RgbaBase64(entry)) =>
      @painter_terminal.encode_rgba_base64_to_kitty_image(
        entry.rgba_base64,
        entry.width,
        entry.height,
        region.width,
        region.height,
        image_id~,
        placement_id~,
      )
    Some(@terminal_image_cache.TerminalImageCacheEntry::RawBase64(b64_data)) =>
      @painter_terminal.encode_png_base64_to_kitty_image(
        @terminal_image_cache.normalize_raster_image_for_kitty(b64_data),
        region.width,
        region.height,
        image_id~,
        placement_id~,
      )
    None => ""
  }
}