///|
extern "js" fn webgpu_preload_font_bytes(font : String, path : String) -> Unit =
  #| (font, path) => {
  #|   const g = globalThis;
  #|   if (!g.__selene_font_bytes_states) {
  #|     g.__selene_font_bytes_states = new Map();
  #|   }
  #|   const states = g.__selene_font_bytes_states;
  #|   const rec = states.get(font);
  #|   if (rec && (rec.status === 0 || rec.status === 1)) {
  #|     return;
  #|   }
  #|   states.set(font, { status: 0, bytes: [] });
  #|   fetch(path)
  #|     .then((res) => {
  #|       if (!res.ok) throw new Error('failed to fetch font');
  #|       return res.arrayBuffer();
  #|     })
  #|     .then((buf) => {
  #|       states.set(font, { status: 1, bytes: Array.from(new Uint8Array(buf)) });
  #|     })
  #|     .catch(() => {
  #|       states.set(font, { status: 2, bytes: [] });
  #|     });
  #| }

///|
extern "js" fn webgpu_font_bytes_status(font : String) -> Int =
  #| (font) => {
  #|   const states = globalThis.__selene_font_bytes_states;
  #|   if (!states) return -1;
  #|   const rec = states.get(font);
  #|   if (!rec) return -1;
  #|   return rec.status | 0;
  #| }

///|
extern "js" fn webgpu_take_font_bytes(font : String) -> Array[Int] =
  #| (font) => {
  #|   const states = globalThis.__selene_font_bytes_states;
  #|   if (!states) return [];
  #|   const rec = states.get(font);
  #|   if (!rec || rec.status !== 1) return [];
  #|   states.set(font, { status: 3, bytes: [] });
  #|   return rec.bytes || [];
  #| }

///|
extern "js" fn resolve_asset_path(path : String) -> String =
  #| (path) => {
  #|   if (
  #|     path.startsWith("http://") ||
  #|     path.startsWith("https://") ||
  #|     path.startsWith("data:") ||
  #|     path.startsWith("blob:")
  #|   ) {
  #|     return path;
  #|   }
  #|   if (!path.startsWith("assets/")) {
  #|     return path;
  #|   }
  #|   const href = globalThis.location?.href;
  #|   const cache =
  #|     globalThis.__selene_webgpu_asset_path_cache ??
  #|     (globalThis.__selene_webgpu_asset_path_cache = new Map());
  #|   const cacheKey = `${href ?? ""}\u0000${path}`;
  #|   const cached = cache.get(cacheKey);
  #|   if (cached !== undefined) {
  #|     return cached;
  #|   }
  #|   try {
  #|     if (!href) return path;
  #|     const resolved = new URL(path, href).pathname;
  #|     cache.set(cacheKey, resolved);
  #|     return resolved;
  #|   } catch (_err) {
  #|     return path;
  #|   }
  #| }

///|
extern "js" fn webgpu_load_file_bytes_sync(path : String) -> Array[Int] =
  #| (path) => {
  #|   globalThis.__selene_webgpu_last_file_load_error = '';
  #|   try {
  #|     const xhr = new XMLHttpRequest();
  #|     xhr.open('GET', path, false);
  #|     // Browsers reject `responseType = "arraybuffer"` for synchronous XHR
  #|     // on document contexts. Use x-user-defined text decoding to preserve
  #|     // raw byte values for sync file reads.
  #|     xhr.overrideMimeType('text/plain; charset=x-user-defined');
  #|     xhr.send(null);
  #|     if (xhr.status >= 200 && xhr.status < 300) {
  #|       const text = xhr.responseText || '';
  #|       const out = new Array(text.length);
  #|       for (let i = 0; i < text.length; i += 1) {
  #|         out[i] = text.charCodeAt(i) & 0xff;
  #|       }
  #|       return out;
  #|     }
  #|     globalThis.__selene_webgpu_last_file_load_error =
  #|       `HTTP ${xhr.status || 0} ${xhr.statusText || 'File not found'}`;
  #|   } catch (err) {
  #|     globalThis.__selene_webgpu_last_file_load_error =
  #|       err && err.message ? String(err.message) : String(err);
  #|     return [];
  #|   }
  #|   return [];
  #| }

///|
extern "js" fn webgpu_last_file_load_error() -> String =
  #| () => String(globalThis.__selene_webgpu_last_file_load_error || '')

///|
extern "js" fn webgpu_gamepad_count() -> Int =
  #| () => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   return pads ? (pads.length | 0) : 0;
  #| }

///|
extern "js" fn webgpu_gamepad_connected(index : Int) -> Bool =
  #| (index) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   if (!pads || index < 0 || index >= pads.length) return false;
  #|   const pad = pads[index];
  #|   return !!(pad && pad.connected);
  #| }

///|
extern "js" fn webgpu_gamepad_button_pressed(index : Int, button : Int) -> Bool =
  #| (index, button) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   const pad = pads?.[index];
  #|   const b = pad?.buttons?.[button];
  #|   return !!(b && b.pressed);
  #| }

///|
extern "js" fn webgpu_gamepad_button_value(index : Int, button : Int) -> Double =
  #| (index, button) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   const pad = pads?.[index];
  #|   const b = pad?.buttons?.[button];
  #|   if (!b) return 0;
  #|   const v = Number(b.value);
  #|   if (!Number.isFinite(v)) return 0;
  #|   if (v < 0) return 0;
  #|   if (v > 1) return 1;
  #|   return v;
  #| }

///|
extern "js" fn webgpu_gamepad_axis_value(index : Int, axis : Int) -> Double =
  #| (index, axis) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   const pad = pads?.[index];
  #|   const a = pad?.axes?.[axis];
  #|   if (a == null) return 0;
  #|   const v = Number(a);
  #|   if (!Number.isFinite(v)) return 0;
  #|   if (v < -1) return -1;
  #|   if (v > 1) return 1;
  #|   return v;
  #| }