///|
extern "js" fn js_request_animation_frame(f : () -> Unit) -> Unit =
  #| (f) => { requestAnimationFrame(() => f()); }

///|
extern "js" fn js_on_beforeunload(f : () -> Unit) -> Unit =
  #| (f) => { window.addEventListener("beforeunload", () => f()); }

///|
extern "js" fn js_performance_now() -> Double =
  #| () => (globalThis.performance?.now?.() ?? Date.now())

///|
pub fn run(
  update~ : (@core.InputSnapshot) -> Unit,
  draw~ : (EngineContext) -> Array[@gfx.DrawTrianglesCommand],
  on_frame? : () -> Unit,
  after_render? : (Double, Double) -> Unit,
  audio_ctx? : @audio.MixerAudioContext,
  audio_frames_per_tick? : Int = 735,
  width? : Int = 320,
  height? : Int = 240,
  title? : String = "kagura",
  canvas? : String = "#app",
) -> Unit {
  invoke_on_start(canvas, title)
  let platform = @platform.create_web_canvas_platform(canvas)
  let surface = platform.current_surface() catch {
    _ => @gfx.create_offscreen_surface_token(width, height)
  }
  let graphics = @gfx.create_webgpu_graphics(
    surface,
    @gfx.default_graphics_backend_options(),
  )
  platform.initialize(
    @platform.new_window_options(title, width, height, false, true),
  ) catch {
    e => println("[engine] platform.initialize failed: \{e}")
  }
  graphics.initialize() catch {
    e => println("[engine] graphics.initialize failed: \{e}")
  }
  let shader_source = @gfx.default_builtin_shader_source()
  let dst = graphics.new_image(width, height) catch {
    e => {
      println("[engine] graphics.new_image failed: \{e}")
      @gfx.new_image_handle(1, width, height)
    }
  }
  let shader = graphics.new_shader(shader_source) catch {
    e => {
      println("[engine] graphics.new_shader failed: \{e}")
      @gfx.new_shader_handle(1, shader_source)
    }
  }
  let ctx : EngineContext = { dst, shader, screen_w: width, screen_h: height }
  let tick : Ref[Int] = Ref(0)
  match audio_ctx {
    Some(_) => {
      let _ = @audio.audio_try_initialize(@audio.default_audio_format())
    }
    None => ()
  }
  // Audio accumulator: tick audio at fixed intervals independent of frame rate
  let audio_tick_ms = audio_frames_per_tick.to_double() / 44100.0 * 1000.0
  let audio_accum : Ref[Double] = Ref(0.0)
  let prev_time : Ref[Double] = Ref(js_performance_now())
  fn frame() {
    platform.poll_events()
    // Resize tracking
    let outside = platform.outside_size()
    let new_w = outside.width.to_int()
    let new_h = outside.height.to_int()
    if new_w > 0 &&
      new_h > 0 &&
      (new_w != ctx.screen_w || new_h != ctx.screen_h) {
      graphics.resize(new_w, new_h) catch {
        e => println("[engine] graphics.resize failed: \{e}")
      }
      ctx.screen_w = new_w
      ctx.screen_h = new_h
      ctx.dst = graphics.new_image(new_w, new_h) catch {
        e => {
          println("[engine] graphics.new_image (resize) failed: \{e}")
          @gfx.new_image_handle(ctx.dst.id, new_w, new_h)
        }
      }
    }
    let input = platform.capture_input(tick.val)
    tick.val = tick.val + 1
    update(input)
    match on_frame {
      Some(f) => f()
      None => ()
    }
    let draw_callback_start = js_performance_now()
    let cmds = draw(ctx)
    let draw_callback_end = js_performance_now()
    let render_start = js_performance_now()
    render_commands(graphics, cmds) catch {
      e => println("[engine] render_commands failed: \{e}")
    }
    let render_end = js_performance_now()
    match after_render {
      Some(f) =>
        f(draw_callback_end - draw_callback_start, render_end - render_start)
      None => ()
    }
    // Time-based audio ticking
    let now = js_performance_now()
    let elapsed = now - prev_time.val
    prev_time.val = now
    match audio_ctx {
      Some(actx) => {
        @audio.audio_resume()
        audio_accum.val = audio_accum.val + elapsed
        while audio_accum.val >= audio_tick_ms {
          let _ = @audio.audio_render_and_write(actx, audio_frames_per_tick)
          actx.tick_bgm(audio_frames_per_tick)
          audio_accum.val = audio_accum.val - audio_tick_ms
        }
      }
      None => ()
    }
    js_request_animation_frame(frame)
  }
  js_on_beforeunload(fn() { invoke_on_stop() })
  js_request_animation_frame(frame)
}