///|
extern "js" fn webgpu_measure_text(
  text : String,
  family : String,
  size : Double,
) -> Array[Double] =
  #| (text, family, size) => {
  #|   const cvs = document.createElement('canvas');
  #|   const ctx = cvs.getContext('2d');
  #|   const fontSize = Number.isFinite(size) && size > 0 ? size : 16;
  #|   const fontFamily = family && family.length > 0 ? family : 'Arial';
  #|   ctx.font = `${fontSize}px ${fontFamily}`;
  #|   const m = ctx.measureText(text ?? '');
  #|   const width = Math.max(0, Number(m.width) || 0);
  #|   const ascent = Math.max(0, Number(m.actualBoundingBoxAscent) || fontSize * 0.8);
  #|   const descent = Math.max(0, Number(m.actualBoundingBoxDescent) || fontSize * 0.2);
  #|   const height = Math.max(1, Math.ceil(ascent + descent));
  #|   return [width, height];
  #| }

///|
extern "js" fn webgpu_begin_frame(
  clear_r : Double,
  clear_g : Double,
  clear_b : Double,
  clear_a : Double,
) -> Unit =
  #| (r, g, b, a) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.beginFrame(r, g, b, a);
  #|   }
  #| }

///|
extern "js" fn webgpu_end_frame() -> Unit =
  #| () => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.endFrame();
  #|   }
  #| }

///|
extern "js" fn webgpu_capture_frame_png_bytes() -> Array[Int] =
  #| () => {
  #|   try {
  #|     const rt = globalThis.__selene_webgpu_runtime;
  #|     const canvas = rt?.canvas;
  #|     if (!canvas || !canvas.toDataURL) return [];
  #|     const dataUrl = canvas.toDataURL("image/png");
  #|     const comma = dataUrl.indexOf(",");
  #|     if (comma < 0) return [];
  #|     const base64 = dataUrl.slice(comma + 1);
  #|     const binary = atob(base64);
  #|     const out = new Array(binary.length);
  #|     for (let i = 0; i < binary.length; i += 1) {
  #|       out[i] = binary.charCodeAt(i) & 0xff;
  #|     }
  #|     return out;
  #|   } catch (_err) {
  #|     return [];
  #|   }
  #| }

///|
extern "js" fn webgpu_preload_img(path : String) -> Unit =
  #| (path) => {
  #|   const g = globalThis;
  #|   if (!g.__selene_webgpu_preload_images) {
  #|     g.__selene_webgpu_preload_images = new Set();
  #|   }
  #|   g.__selene_webgpu_preload_images.add(String(path));
  #|   const rt = g.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.ensureImage(path);
  #|   }
  #| }

///|
extern "js" fn webgpu_render_runtime_ready() -> Bool =
  #| () => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   return !!(rt?.ready && rt?.device && rt?.context && rt?.canvas);
  #| }

///|
extern "js" fn webgpu_resize_capture_surface(width : Int, height : Int) -> Unit =
  #| (width, height) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (!rt?.canvas) return;
  #|   const w = Math.max(1, Math.floor(Number(width) || 1));
  #|   const h = Math.max(1, Math.floor(Number(height) || 1));
  #|   rt.canvas.width = w;
  #|   rt.canvas.height = h;
  #|   if (rt.ready && rt.context && rt.device && rt.format) {
  #|     rt.context.configure({
  #|       device: rt.device,
  #|       format: rt.format,
  #|       alphaMode: 'premultiplied',
  #|     });
  #|     rt.ensureDepth?.();
  #|   }
  #| }

///|
extern "js" fn webgpu_render_pending_resource_count() -> Int =
  #| () => {
  #|   const g = globalThis;
  #|   const rt = g.__selene_webgpu_runtime;
  #|   let pending = 0;
  #|   const preloads = g.__selene_webgpu_preload_images;
  #|   if (!rt && preloads?.size) {
  #|     pending += preloads.size;
  #|   }
  #|   if (rt?.imageCache) {
  #|     for (const rec of rt.imageCache.values()) {
  #|       if (rec?.state === 'loading') pending += 1;
  #|     }
  #|     for (const path of preloads ?? []) {
  #|       if (!rt.imageCache.has(path)) pending += 1;
  #|     }
  #|   }
  #|   const fontStates = g.__selene_font_bytes_states;
  #|   for (const rec of fontStates?.values?.() ?? []) {
  #|     if (rec?.status === 0) pending += 1;
  #|   }
  #|   return pending | 0;
  #| }

///|
extern "js" fn webgpu_render_resource_errors() -> Array[String] =
  #| () => {
  #|   const g = globalThis;
  #|   const rt = g.__selene_webgpu_runtime;
  #|   const errors = [];
  #|   if (rt?.imageCache) {
  #|     for (const [path, rec] of rt.imageCache.entries()) {
  #|       if (rec?.state === 'error') {
  #|         errors.push(`image:${String(path)}`);
  #|       }
  #|     }
  #|   }
  #|   const fontStates = g.__selene_font_bytes_states;
  #|   for (const [family, rec] of fontStates?.entries?.() ?? []) {
  #|     if (rec?.status === 2) {
  #|       errors.push(`font:${String(family)}`);
  #|     }
  #|   }
  #|   return errors;
  #| }

///|
extern "js" fn webgpu_begin_2d_pass(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  load_op : Int,
  clear_r : Double,
  clear_g : Double,
  clear_b : Double,
  clear_a : Double,
  target_name : String,
) -> Unit =
  #| (x, y, width, height, loadOp, clearR, clearG, clearB, clearA, targetName) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.beginPass2d(x, y, width, height, loadOp, [clearR, clearG, clearB, clearA], targetName);
  #|   }
  #| }

///|
extern "js" fn webgpu_end_2d_pass() -> Unit =
  #| () => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.endPass2d();
  #|   }
  #| }

///|
extern "js" fn webgpu_push_clip_rect(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> Unit =
  #| (x, y, width, height) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushClip2d(x, y, width, height);
  #|   }
  #| }