///|
pub(all) struct RgbaCacheEntry {
  width : Int
  height : Int
  rgba_base64 : String
}

///|
fn make_substr(chars : Array[Char], start : Int, end : Int) -> String {
  let buf = StringBuilder::new()
  for i = start; i < end && i < chars.length(); i = i + 1 {
    buf.write_char(chars[i])
  }
  buf.to_string()
}

///|
pub fn RgbaCacheEntry::encode(self : RgbaCacheEntry) -> String {
  self.width.to_string() +
  ":" +
  self.height.to_string() +
  ":" +
  self.rgba_base64
}

///|
pub fn RgbaCacheEntry::decode_rgba_bytes(self : RgbaCacheEntry) -> Array[Int]? {
  decode_base64_bytes(self.rgba_base64)
}

///|
fn decode_base64_char(c : Char) -> Int? {
  let cp = c.to_int()
  if cp >= 'A'.to_int() && cp <= 'Z'.to_int() {
    Some(cp - 'A'.to_int())
  } else if cp >= 'a'.to_int() && cp <= 'z'.to_int() {
    Some(cp - 'a'.to_int() + 26)
  } else if cp >= '0'.to_int() && cp <= '9'.to_int() {
    Some(cp - '0'.to_int() + 52)
  } else if c == '+' {
    Some(62)
  } else if c == '/' {
    Some(63)
  } else {
    None
  }
}

///|
fn decode_base64_bytes(encoded : String) -> Array[Int]? {
  let bytes : Array[Int] = []
  let quartet : Array[Int] = []
  for c in encoded {
    if c.is_ascii_whitespace() {
      continue
    }
    if c == '=' {
      quartet.push(-1)
    } else {
      match decode_base64_char(c) {
        Some(v) => quartet.push(v)
        None => return None
      }
    }
    if quartet.length() == 4 {
      let v0 = quartet[0]
      let v1 = quartet[1]
      let v2 = quartet[2]
      let v3 = quartet[3]
      if v0 < 0 || v1 < 0 {
        return None
      }
      bytes.push(((v0 << 2) | (v1 >> 4)) & 0xFF)
      if v2 >= 0 {
        bytes.push((((v1 & 0x0F) << 4) | (v2 >> 2)) & 0xFF)
      }
      if v2 >= 0 && v3 >= 0 {
        bytes.push((((v2 & 0x03) << 6) | v3) & 0xFF)
      }
      if v2 < 0 || v3 < 0 {
        break
      }
      quartet.clear()
    }
  }
  Some(bytes)
}

///|
pub(all) enum TerminalImageCacheEntry {
  Svg(String)
  PngBase64(String)
  RgbaBase64(RgbaCacheEntry)
  RawBase64(String)
}

///|
pub fn TerminalImageCacheEntry::encode(
  self : TerminalImageCacheEntry,
) -> String {
  match self {
    Svg(svg_text) => "svg:" + svg_text
    PngBase64(b64_data) => "img_png:" + b64_data
    RgbaBase64(entry) => "img_rgba:" + entry.encode()
    RawBase64(b64_data) => "img:" + b64_data
  }
}

///|
fn parse_int_ascii(s : String) -> Int {
  let mut result = 0
  for c in s {
    if c >= '0' && c <= '9' {
      result = result * 10 + (c.to_int() - '0'.to_int())
    }
  }
  result
}

///|
fn parse_kitty_rgba_cache_entry(entry : String) -> RgbaCacheEntry? {
  match entry.find(":") {
    Some(width_end) => {
      let width_str = entry.unsafe_substring(start=0, end=width_end)
      let rest = entry.unsafe_substring(start=width_end + 1, end=entry.length())
      match rest.find(":") {
        Some(height_end) => {
          let height_str = rest.unsafe_substring(start=0, end=height_end)
          let rgba_base64 = rest.unsafe_substring(
            start=height_end + 1,
            end=rest.length(),
          )
          let width = parse_int_ascii(width_str)
          let height = parse_int_ascii(height_str)
          if width > 0 && height > 0 && rgba_base64.length() > 0 {
            Some({ width, height, rgba_base64 })
          } else {
            None
          }
        }
        None => None
      }
    }
    None => None
  }
}

///|
pub fn parse_terminal_image_cache_entry(
  cached_data : String,
) -> TerminalImageCacheEntry? {
  if cached_data.has_prefix("svg:") {
    Some(Svg(cached_data.unsafe_substring(start=4, end=cached_data.length())))
  } else if cached_data.has_prefix("img_png:") {
    Some(
      PngBase64(cached_data.unsafe_substring(start=8, end=cached_data.length())),
    )
  } else if cached_data.has_prefix("img_rgba:") {
    match
      parse_kitty_rgba_cache_entry(
        cached_data.unsafe_substring(start=9, end=cached_data.length()),
      ) {
      Some(entry) => Some(RgbaBase64(entry))
      None => None
    }
  } else if cached_data.has_prefix("img:") {
    Some(
      RawBase64(cached_data.unsafe_substring(start=4, end=cached_data.length())),
    )
  } else {
    None
  }
}

///|
pub fn detect_raster_image_format_from_base64(b64_data : String) -> String {
  if b64_data.has_prefix("iVBORw0KGgo") {
    "png"
  } else if b64_data.has_prefix("/9j/") {
    "jpeg"
  } else if b64_data.has_prefix("R0lGOD") {
    "gif"
  } else {
    "unknown"
  }
}

///|
#cfg(target="js")
extern "js" fn js_decode_raster_image_to_rgba_base64(
  image_format : String,
  b64_data : String,
) -> String =
  #| (imageFormat, b64Data) => {
  #|   try {
  #|     if (typeof process === 'undefined') {
  #|       return '';
  #|     }
  #|     const format = String(imageFormat || '').toLowerCase();
  #|     if (format !== 'png' && format !== 'jpeg' && format !== 'gif') {
  #|       return '';
  #|     }
  #|     const fs = require('node:fs');
  #|     const os = require('node:os');
  #|     const path = require('node:path');
  #|     const { execFileSync } = require('node:child_process');
  #|     function loadPng() {
  #|       try {
  #|         return require('pngjs').PNG;
  #|       } catch (_) {
  #|         const candidates = [
  #|           path.join(process.cwd(), 'node_modules', 'pngjs'),
  #|           path.join(process.cwd(), 'browser', 'node_modules', 'pngjs'),
  #|         ];
  #|         for (const candidate of candidates) {
  #|           try {
  #|             return require(candidate).PNG;
  #|           } catch (_) {}
  #|         }
  #|         throw new Error('pngjs not found');
  #|       }
  #|     }
  #|     const PNG = loadPng();
  #|     if (format === 'png') {
  #|       const decoded = PNG.sync.read(Buffer.from(String(b64Data || ''), 'base64'));
  #|       return (
  #|         String(decoded.width) +
  #|         ':' +
  #|         String(decoded.height) +
  #|         ':' +
  #|         Buffer.from(decoded.data).toString('base64')
  #|       );
  #|     }
  #|     if (process.platform !== 'darwin') {
  #|       return '';
  #|     }
  #|     const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'crater-kitty-'));
  #|     const inputExt = format === 'jpeg' ? 'jpg' : 'gif';
  #|     const inputPath = path.join(dir, 'input.' + inputExt);
  #|     const outputPath = path.join(dir, 'output.png');
  #|     try {
  #|       fs.writeFileSync(inputPath, Buffer.from(String(b64Data || ''), 'base64'));
  #|       execFileSync(
  #|         'sips',
  #|         ['-s', 'format', 'png', inputPath, '--out', outputPath],
  #|         { stdio: 'pipe' },
  #|       );
  #|       const pngBuffer = fs.readFileSync(outputPath);
  #|       const decoded = PNG.sync.read(pngBuffer);
  #|       return (
  #|         String(decoded.width) +
  #|         ':' +
  #|         String(decoded.height) +
  #|         ':' +
  #|         Buffer.from(decoded.data).toString('base64')
  #|       );
  #|     } finally {
  #|       try {
  #|         fs.rmSync(dir, { recursive: true, force: true });
  #|       } catch (_) {}
  #|     }
  #|   } catch (_) {
  #|     return '';
  #|   }
  #| }

///|
#cfg(target="js")
fn decode_raster_image_to_rgba_base64(
  image_format : String,
  b64_data : String,
) -> String? {
  let decoded = js_decode_raster_image_to_rgba_base64(image_format, b64_data)
  if decoded.length() > 0 {
    Some(decoded)
  } else {
    None
  }
}

///|
#cfg(target="native")
fn decode_raster_image_to_rgba_base64(
  _image_format : String,
  _b64_data : String,
) -> String? {
  None
}

///|
pub fn decode_raster_image_to_rgba_cache_entry(
  image_format : String,
  b64_data : String,
) -> RgbaCacheEntry? {
  match decode_raster_image_to_rgba_base64(image_format, b64_data) {
    Some(entry) => parse_kitty_rgba_cache_entry(entry)
    None => None
  }
}

///|
#cfg(target="js")
extern "js" fn js_transcode_raster_image_to_png_base64(
  image_format : String,
  b64_data : String,
) -> String =
  #| (imageFormat, b64Data) => {
  #|   try {
  #|     if (typeof process === 'undefined' || process.platform !== 'darwin') {
  #|       return '';
  #|     }
  #|     const format = String(imageFormat || '').toLowerCase();
  #|     if (format !== 'jpeg' && format !== 'gif') {
  #|       return '';
  #|     }
  #|     const fs = require('node:fs');
  #|     const os = require('node:os');
  #|     const path = require('node:path');
  #|     const { execFileSync } = require('node:child_process');
  #|     const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'crater-kitty-'));
  #|     const inputExt = format === 'jpeg' ? 'jpg' : 'gif';
  #|     const inputPath = path.join(dir, 'input.' + inputExt);
  #|     const outputPath = path.join(dir, 'output.png');
  #|     try {
  #|       fs.writeFileSync(inputPath, Buffer.from(String(b64Data || ''), 'base64'));
  #|       execFileSync(
  #|         'sips',
  #|         ['-s', 'format', 'png', inputPath, '--out', outputPath],
  #|         { stdio: 'pipe' },
  #|       );
  #|       return fs.readFileSync(outputPath).toString('base64');
  #|     } finally {
  #|       try {
  #|         fs.rmSync(dir, { recursive: true, force: true });
  #|       } catch (_) {}
  #|     }
  #|   } catch (_) {
  #|     return '';
  #|   }
  #| }

///|
#cfg(target="js")
fn transcode_raster_image_to_png_base64(
  image_format : String,
  b64_data : String,
) -> String? {
  let transcoded = js_transcode_raster_image_to_png_base64(
    image_format, b64_data,
  )
  if transcoded.length() > 0 {
    Some(transcoded)
  } else {
    None
  }
}

///|
#cfg(target="native")
fn transcode_raster_image_to_png_base64(
  _image_format : String,
  _b64_data : String,
) -> String? {
  None
}

///|
pub fn normalize_raster_image_for_kitty(b64_data : String) -> String {
  let image_format = detect_raster_image_format_from_base64(b64_data)
  if image_format == "png" {
    return b64_data
  }
  match transcode_raster_image_to_png_base64(image_format, b64_data) {
    Some(transcoded) => transcoded
    None => b64_data
  }
}

///|
pub fn prepare_raster_image_for_kitty_cache(b64_data : String) -> String {
  let image_format = detect_raster_image_format_from_base64(b64_data)
  if image_format == "png" {
    return TerminalImageCacheEntry::PngBase64(b64_data).encode()
  }
  match decode_raster_image_to_rgba_cache_entry(image_format, b64_data) {
    Some(entry) => TerminalImageCacheEntry::RgbaBase64(entry).encode()
    None => {
      let normalized = normalize_raster_image_for_kitty(b64_data)
      if detect_raster_image_format_from_base64(normalized) == "png" {
        TerminalImageCacheEntry::PngBase64(normalized).encode()
      } else {
        TerminalImageCacheEntry::RawBase64(b64_data).encode()
      }
    }
  }
}

///|
fn hex_digit_to_int(c : Char) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - '0'.to_int())
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 'a'.to_int() + 10)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 'A'.to_int() + 10)
  } else {
    None
  }
}

///|
fn percent_decode_data_url_payload(payload : String) -> String {
  let chars = payload.to_array()
  let buf = StringBuilder::new()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' && i + 2 < chars.length() {
      match (hex_digit_to_int(chars[i + 1]), hex_digit_to_int(chars[i + 2])) {
        (Some(high), Some(low)) => {
          buf.write_char((high * 16 + low).unsafe_to_char())
          i = i + 3
        }
        _ => {
          buf.write_char(chars[i])
          i = i + 1
        }
      }
    } else {
      buf.write_char(chars[i])
      i = i + 1
    }
  }
  buf.to_string()
}

///|
fn data_url_payload_end(chars : Array[Char], start : Int) -> Int {
  let mut end = chars.length()
  for i = start; i < chars.length(); i = i + 1 {
    if chars[i] == '?' || chars[i] == '#' {
      end = i
      break
    }
  }
  end
}

///|
fn remove_ascii_whitespace(s : String) -> String {
  let buf = StringBuilder::new()
  for c in s {
    if !c.is_ascii_whitespace() {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
pub fn data_image_src_to_kitty_cache(src : String) -> String? {
  if !src.to_lower().has_prefix("data:") {
    return None
  }
  let chars = src.to_array()
  let mut comma_idx = -1
  for i = 5; i < chars.length(); i = i + 1 {
    if chars[i] == ',' {
      comma_idx = i
      break
    }
  }
  if comma_idx < 0 {
    return None
  }
  let header = make_substr(chars, 5, comma_idx).to_lower()
  if !header.has_prefix("image/") {
    return None
  }
  let payload_end = data_url_payload_end(chars, comma_idx + 1)
  let payload = make_substr(chars, comma_idx + 1, payload_end)
  if header.has_prefix("image/svg+xml") {
    if header.contains(";base64") {
      return None
    }
    return Some(
      TerminalImageCacheEntry::Svg(percent_decode_data_url_payload(payload)).encode(),
    )
  }
  if !header.contains(";base64") {
    return None
  }
  if header.has_prefix("image/png") ||
    header.has_prefix("image/jpeg") ||
    header.has_prefix("image/jpg") ||
    header.has_prefix("image/gif") ||
    header.has_prefix("image/webp") {
    Some(prepare_raster_image_for_kitty_cache(remove_ascii_whitespace(payload)))
  } else {
    None
  }
}