//! Polymodel renderer Web Worker — wasm entry point and all rendering logic.
//!
//! This module is only compiled for `wasm32-unknown-unknown`. It owns the
//! `requestAnimationFrame` frame loop, the `OffscreenCanvas` GL context, mesh loading,
//! and the postcard message protocol bridge to the Dioxus main thread.

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use wasm_bindgen_futures::spawn_local;
use web_sys::DedicatedWorkerGlobalScope;

use polymodel_mesh::companions::{discover_companion_uris, is_safe_relative_uri};
use polymodel_mesh::contract::ModelMesh;
use polymodel_mesh::parser::{DefaultMeshParser, Local, MeshParser, MeshResources, MeshSource};
use polymodel_renderer_protocol::{
    KeyModifiers, MeshFormat, MeshStats, RendererCommand, RendererEvent, deserialize_command,
    serialize_event,
};

// ---------------------------------------------------------------------------
// Allocator
// ---------------------------------------------------------------------------

use talc::{sync::TalcLock, wasm::*};

#[global_allocator]
static TALC: TalcLock<spinning_top::RawSpinlock, WasmGrowAndClaim, WasmBinning> =
    TalcLock::new(WasmGrowAndClaim);

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------

#[derive(Default)]
struct InputState {
    x: f32,
    y: f32,
    buttons: u16,
    modifiers: KeyModifiers,
    wheel_delta: f32,
    button_down: bool,
    last_consumed: Option<(f32, f32)>,
}

struct RendererState {
    context: three_d::Context,
    mesh: three_d::Gm<three_d::Mesh, three_d::PhysicalMaterial>,
    ambient_light: three_d::AmbientLight,
    key_light: three_d::DirectionalLight,
    fill_light: three_d::DirectionalLight,
    camera: three_d::Camera,
    control: three_d::OrbitControl,
}

struct WorkerState {
    scope: DedicatedWorkerGlobalScope,
    canvas: Option<web_sys::OffscreenCanvas>,
    renderer: Option<RendererState>,
    pixel_width: u32,
    pixel_height: u32,
    input: InputState,
    needs_render: bool,
    disposed: bool,
    load_epoch: u32,
    raf_id: Option<i32>,
    raf_closure: Option<Closure<dyn FnMut()>>,
}

impl WorkerState {
    fn new(scope: DedicatedWorkerGlobalScope) -> Self {
        Self {
            scope,
            canvas: None,
            renderer: None,
            pixel_width: 1,
            pixel_height: 1,
            input: InputState::default(),
            needs_render: false,
            disposed: false,
            load_epoch: 0,
            raf_id: None,
            raf_closure: None,
        }
    }

    fn post_event(&self, evt: &RendererEvent) {
        match serialize_event(evt) {
            Ok(bytes) => {
                let array = js_sys::Uint8Array::from(&bytes[..]);
                let _ = self.scope.post_message(&array);
            }
            Err(e) => tracing::error!("failed to serialize event {evt:?}: {e}"),
        }
    }

    fn init_renderer(&mut self, canvas: web_sys::OffscreenCanvas) {
        let opts = js_sys::Object::new();
        let _ = js_sys::Reflect::set(&opts, &"desynchronized".into(), &JsValue::from_bool(true));

        let gl = match canvas.get_context_with_context_options("webgl2", &opts) {
            Ok(Some(val)) => val.dyn_into::<web_sys::WebGl2RenderingContext>(),
            _ => {
                self.post_event(&RendererEvent::Error {
                    fatal: true,
                    message: "WebGL2 context could not be created on OffscreenCanvas".into(),
                });
                return;
            }
        };

        let gl = match gl {
            Ok(gl) => gl,
            Err(_) => {
                self.post_event(&RendererEvent::Error {
                    fatal: true,
                    message: "OffscreenCanvas context is not WebGL2".into(),
                });
                return;
            }
        };

        let glow_context = three_d::context::Context::from_webgl2_context(gl);
        let context = match three_d::Context::from_gl_context(Arc::new(glow_context)) {
            Ok(ctx) => ctx,
            Err(e) => {
                self.post_event(&RendererEvent::Error {
                    fatal: true,
                    message: format!("three-d context error: {e}"),
                });
                return;
            }
        };

        let (ambient_light, key_light, fill_light) = standard_lights(&context);

        let placeholder = build_placeholder_mesh(&context);
        let viewport = three_d::Viewport::new_at_origo(self.pixel_width, self.pixel_height);
        let camera = three_d::Camera::new_perspective(
            viewport,
            three_d::vec3(3.0, -4.0, 2.0),
            three_d::vec3(0.0, 0.0, 0.0),
            three_d::vec3(0.0, 0.0, 1.0),
            three_d::degrees(45.0),
            0.1,
            100.0,
        );
        let control = three_d::OrbitControl::new(three_d::vec3(0.0, 0.0, 0.0), 0.1, 10.0);

        self.canvas = Some(canvas);
        self.renderer = Some(RendererState {
            context,
            mesh: placeholder,
            ambient_light,
            key_light,
            fill_light,
            camera,
            control,
        });
        self.post_event(&RendererEvent::ContextAcquired);
        self.post_event(&RendererEvent::RendererReady);
        self.needs_render = true;
    }
}

fn build_placeholder_mesh(
    context: &three_d::Context,
) -> three_d::Gm<three_d::Mesh, three_d::PhysicalMaterial> {
    let mut mesh = three_d_asset::TriMesh {
        positions: three_d_asset::Positions::F32(vec![
            three_d_asset::Vec3::new(0.0, 0.0, 0.18),
            three_d_asset::Vec3::new(-0.16, -0.12, -0.08),
            three_d_asset::Vec3::new(0.16, -0.12, -0.08),
            three_d_asset::Vec3::new(0.0, 0.18, -0.08),
        ]),
        indices: three_d_asset::Indices::U32(vec![0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2]),
        normals: None,
        tangents: None,
        uvs: None,
        colors: None,
    };
    mesh.compute_normals();

    three_d::Gm::new(
        three_d::Mesh::new(context, &mesh),
        three_d::PhysicalMaterial {
            albedo: three_d::Srgba::new_opaque(116, 180, 255),
            roughness: 0.72,
            metallic: 0.0,
            ..Default::default()
        },
    )
}

// ---------------------------------------------------------------------------
// Scene building
// ---------------------------------------------------------------------------

fn build_scene(
    context: &three_d::Context,
    model: &ModelMesh,
    pixel_width: u32,
    pixel_height: u32,
) -> (
    three_d::Gm<three_d::Mesh, three_d::PhysicalMaterial>,
    three_d::Camera,
    three_d::OrbitControl,
) {
    let fit = model.camera_fit();
    let mesh = three_d::Gm::new(
        three_d::Mesh::new(context, &model.trimesh),
        three_d::PhysicalMaterial {
            albedo: three_d::Srgba::new_opaque(116, 180, 255),
            roughness: 0.72,
            metallic: 0.0,
            ..Default::default()
        },
    );
    let radius = fit.radius.max(0.001);
    let target = three_d::vec3(fit.center[0], fit.center[1], fit.center[2]);
    let position = target + three_d::vec3(radius * 1.8, -radius * 2.4, radius * 1.6);
    let viewport = three_d::Viewport::new_at_origo(pixel_width, pixel_height);
    let camera = three_d::Camera::new_perspective(
        viewport,
        position,
        target,
        three_d::vec3(0.0, 0.0, 1.0),
        three_d::degrees(45.0),
        (radius / 1000.0).max(0.000001),
        radius * 100.0,
    );
    let control = three_d::OrbitControl::new(target, radius * 0.01, radius * 50.0);
    (mesh, camera, control)
}

fn standard_lights(
    context: &three_d::Context,
) -> (
    three_d::AmbientLight,
    three_d::DirectionalLight,
    three_d::DirectionalLight,
) {
    let ambient_light =
        three_d::AmbientLight::new(context, 0.45, three_d::Srgba::new_opaque(255, 255, 255));
    let key_light = three_d::DirectionalLight::new(
        context,
        2.6,
        three_d::Srgba::new_opaque(255, 255, 255),
        three_d::vec3(-0.45, -0.55, -0.70),
    );
    let fill_light = three_d::DirectionalLight::new(
        context,
        0.9,
        three_d::Srgba::new_opaque(170, 205, 255),
        three_d::vec3(0.55, 0.35, -0.35),
    );
    (ambient_light, key_light, fill_light)
}

// ---------------------------------------------------------------------------
// Frame loop
// ---------------------------------------------------------------------------

fn start_frame_loop(state: &Rc<RefCell<WorkerState>>) {
    let state_for_raf = state.clone();
    let closure = Closure::wrap(Box::new(move || {
        let mut st = match state_for_raf.try_borrow_mut() {
            Ok(st) => st,
            Err(_) => return,
        };
        st.raf_id = None;

        if st.disposed {
            return;
        }

        if st.needs_render && st.renderer.is_some() {
            consume_input_and_render(&mut st);
            st.needs_render = false;
        }

        schedule_next_frame(&mut st, state_for_raf.clone());
    }) as Box<dyn FnMut()>);

    let mut st = state.borrow_mut();
    st.raf_closure = Some(closure);
    schedule_next_frame(&mut st, state.clone());
}

fn schedule_next_frame(st: &mut WorkerState, state: Rc<RefCell<WorkerState>>) {
    if st.raf_id.is_some() || st.disposed {
        return;
    }
    let scope = st.scope.clone();
    let closure_ref = st.raf_closure.as_ref().expect("raf closure exists");
    match scope.request_animation_frame(closure_ref.as_ref().unchecked_ref()) {
        Ok(id) => st.raf_id = Some(id),
        Err(_) => {
            let state_for_timeout = state.clone();
            let timeout_closure = Closure::wrap(Box::new(move || {
                let st = state_for_timeout.borrow();
                if st.disposed || st.raf_id.is_some() {
                    return;
                }
                drop(st);
                let mut st = state_for_timeout.borrow_mut();
                let scope = st.scope.clone();
                if let Some(closure) = st.raf_closure.as_ref() {
                    if let Ok(id) = scope.request_animation_frame(closure.as_ref().unchecked_ref())
                    {
                        st.raf_id = Some(id);
                    }
                }
            }) as Box<dyn FnMut()>);
            let scope = st.scope.clone();
            let _ = scope.set_timeout_with_callback_and_timeout_and_arguments_0(
                timeout_closure.as_ref().unchecked_ref(),
                16,
            );
            timeout_closure.forget();
        }
    }
}

fn consume_input_and_render(st: &mut WorkerState) {
    let renderer = match st.renderer.as_mut() {
        Some(r) => r,
        None => return,
    };

    let mut events: Vec<three_d::Event> = Vec::new();

    // Read input into locals to avoid borrowing st.input across the assignment below.
    let (x, y, button_down, last_consumed, wheel_delta) = {
        let input = &st.input;
        (
            input.x,
            input.y,
            input.button_down,
            input.last_consumed,
            input.wheel_delta,
        )
    };

    if button_down {
        if let Some((last_x, last_y)) = last_consumed {
            let dx = x - last_x;
            let dy = y - last_y;
            if dx != 0.0 || dy != 0.0 {
                events.push(three_d::Event::MouseMotion {
                    button: Some(three_d::MouseButton::Left),
                    delta: (dx, dy),
                    position: (x, y).into(),
                    modifiers: Default::default(),
                    handled: false,
                });
            }
        }
        st.input.last_consumed = Some((x, y));
    }

    if wheel_delta != 0.0 {
        events.push(three_d::Event::MouseWheel {
            delta: (0.0, -wheel_delta),
            position: (x, y).into(),
            modifiers: Default::default(),
            handled: false,
        });
        st.input.wheel_delta = 0.0;
    }

    if !events.is_empty() {
        renderer
            .control
            .handle_events(&mut renderer.camera, &mut events);
    }

    let viewport = three_d::Viewport::new_at_origo(st.pixel_width, st.pixel_height);
    renderer.camera.set_viewport(viewport);

    let screen = three_d::RenderTarget::screen(&renderer.context, st.pixel_width, st.pixel_height);
    let _ = screen
        .clear(three_d::ClearState::color_and_depth(
            0.035, 0.043, 0.067, 1.0, 1.0,
        ))
        .render(
            &renderer.camera,
            std::slice::from_ref(&renderer.mesh),
            &[
                &renderer.ambient_light,
                &renderer.key_light,
                &renderer.fill_light,
            ],
        );
}

// ---------------------------------------------------------------------------
// LoadMesh (async)
// ---------------------------------------------------------------------------

fn handle_load_mesh(state: &Rc<RefCell<WorkerState>>, format: MeshFormat, primary_url: String) {
    let epoch = {
        let mut st = state.borrow_mut();
        if st.disposed {
            return;
        }
        st.load_epoch += 1;
        st.load_epoch
    };

    let state_clone = state.clone();
    spawn_local(async move {
        let result = load_mesh_async(&primary_url, format).await;

        let mut st = match state_clone.try_borrow_mut() {
            Ok(st) => st,
            Err(_) => return,
        };

        if st.disposed || st.load_epoch != epoch {
            return;
        }

        match result {
            Ok(model) => {
                let stats = MeshStats {
                    vertices: model.vertex_count(),
                    triangles: model.triangle_count(),
                };

                let pw = st.pixel_width;
                let ph = st.pixel_height;
                if let Some(renderer) = st.renderer.as_mut() {
                    let (mesh, camera, control) = build_scene(&renderer.context, &model, pw, ph);
                    renderer.mesh = mesh;
                    renderer.camera = camera;
                    renderer.control = control;
                }

                st.needs_render = true;
                st.post_event(&RendererEvent::MeshLoaded { stats });
            }
            Err(msg) => {
                st.post_event(&RendererEvent::MeshError { message: msg });
            }
        }
    });
}

async fn load_mesh_async(primary_url: &str, format: MeshFormat) -> Result<ModelMesh, String> {
    if format == MeshFormat::Threemf {
        return Err("unsupported mesh format".into());
    }

    let primary_bytes = fetch_bytes(primary_url)
        .await
        .map_err(|e| format!("fetch primary: {e}"))?;

    let companions = if format == MeshFormat::Gltf {
        let uris = discover_companion_uris(&primary_bytes);
        let mut comps: Vec<(String, Vec<u8>)> = Vec::new();
        for uri in &uris {
            if !is_safe_relative_uri(uri) {
                return Err(format!("unsafe companion URI: {uri}"));
            }
            let base = primary_url
                .rsplit_once('/')
                .map(|(base, _)| base)
                .unwrap_or("");
            let companion_url = if base.is_empty() {
                uri.clone()
            } else {
                format!("{base}/{uri}")
            };
            let bytes = fetch_bytes(&companion_url)
                .await
                .map_err(|e| format!("fetch companion {uri}: {e}"))?;
            comps.push((uri.clone(), bytes));
        }
        comps
    } else {
        Vec::new()
    };

    let source = MeshSource {
        format,
        primary: &primary_bytes,
        resources: MeshResources::new(&companions),
    };

    let parser = DefaultMeshParser::<Local>::default();
    parser.parse(&source).await.map_err(|e| e.to_string())
}

// ---------------------------------------------------------------------------
// RenderPreviewImage (async)
// ---------------------------------------------------------------------------

fn handle_render_preview_image(
    state: &Rc<RefCell<WorkerState>>,
    request_id: u32,
    format: MeshFormat,
    primary_url: String,
    width: u32,
    height: u32,
) {
    let scope = state.borrow().scope.clone();
    spawn_local(async move {
        let result = async {
            let model = load_mesh_async(&primary_url, format).await?;
            render_preview_png(&model, width.max(1), height.max(1)).await
        }
        .await;

        let evt = match result {
            Ok(png) => RendererEvent::PreviewImageRendered { request_id, png },
            Err(message) => RendererEvent::PreviewImageError {
                request_id,
                message,
            },
        };
        match serialize_event(&evt) {
            Ok(bytes) => {
                let array = js_sys::Uint8Array::from(&bytes[..]);
                let _ = scope.post_message(&array);
            }
            Err(e) => tracing::error!("failed to serialize preview event {evt:?}: {e}"),
        }
    });
}

async fn render_preview_png(model: &ModelMesh, width: u32, height: u32) -> Result<Vec<u8>, String> {
    let canvas = web_sys::OffscreenCanvas::new(width, height)
        .map_err(|e| format!("preview canvas: {e:?}"))?;
    let opts = js_sys::Object::new();
    js_sys::Reflect::set(
        &opts,
        &"preserveDrawingBuffer".into(),
        &JsValue::from_bool(true),
    )
    .map_err(|e| format!("preview context options: {e:?}"))?;
    let gl = canvas
        .get_context_with_context_options("webgl2", &opts)
        .map_err(|e| format!("preview context: {e:?}"))?
        .ok_or_else(|| "preview WebGL2 context unavailable".to_string())?
        .dyn_into::<web_sys::WebGl2RenderingContext>()
        .map_err(|_| "preview context is not WebGL2".to_string())?;

    let glow_context = three_d::context::Context::from_webgl2_context(gl);
    let context = three_d::Context::from_gl_context(Arc::new(glow_context))
        .map_err(|e| format!("preview three-d context: {e}"))?;
    let (mesh, camera, _control) = build_scene(&context, model, width, height);
    let (ambient_light, key_light, fill_light) = standard_lights(&context);
    let screen = three_d::RenderTarget::screen(&context, width, height);
    let _ = screen
        .clear(three_d::ClearState::color_and_depth(
            0.035, 0.043, 0.067, 1.0, 1.0,
        ))
        .render(&camera, [&mesh], &[&ambient_light, &key_light, &fill_light]);

    offscreen_canvas_png_bytes(&canvas).await
}

async fn offscreen_canvas_png_bytes(canvas: &web_sys::OffscreenCanvas) -> Result<Vec<u8>, String> {
    let options = js_sys::Object::new();
    js_sys::Reflect::set(&options, &"type".into(), &JsValue::from_str("image/png"))
        .map_err(|e| format!("preview blob options: {e:?}"))?;
    let convert = js_sys::Reflect::get(canvas, &"convertToBlob".into())
        .map_err(|e| format!("preview convertToBlob lookup: {e:?}"))?
        .dyn_into::<js_sys::Function>()
        .map_err(|_| "preview convertToBlob is unavailable".to_string())?;
    let promise = convert
        .call1(canvas, &options)
        .map_err(|e| format!("preview convertToBlob: {e:?}"))?
        .dyn_into::<js_sys::Promise>()
        .map_err(|_| "preview convertToBlob did not return a Promise".to_string())?;
    let blob = JsFuture::from(promise)
        .await
        .map_err(|e| format!("preview blob await: {e:?}"))?
        .dyn_into::<web_sys::Blob>()
        .map_err(|_| "preview result is not a Blob".to_string())?;
    let buffer = JsFuture::from(blob.array_buffer())
        .await
        .map_err(|e| format!("preview blob arrayBuffer: {e:?}"))?;
    Ok(js_sys::Uint8Array::new(&buffer).to_vec())
}

async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
    let opts = web_sys::RequestInit::new();
    opts.set_method("GET");
    opts.set_mode(web_sys::RequestMode::SameOrigin);

    let request = web_sys::Request::new_with_str_and_init(url, &opts)
        .map_err(|e| format!("request: {:?}", e))?;

    // In a worker, `self` is the global scope; use `fetch_with_request` from the
    // WorkerGlobalScope rather than `window()`.
    let global = js_sys::global();
    let fetch_fn =
        js_sys::Reflect::get(&global, &"fetch".into()).map_err(|e| format!("no fetch: {:?}", e))?;

    let fetch_promise = js_sys::Function::from(fetch_fn)
        .call1(&global, &request)
        .map_err(|e| format!("fetch call: {:?}", e))?;

    let fetch_promise = fetch_promise
        .dyn_into::<js_sys::Promise>()
        .map_err(|_| "fetch: not a Promise".to_string())?;

    let response = wasm_bindgen_futures::JsFuture::from(fetch_promise)
        .await
        .map_err(|e| format!("fetch: {:?}", e))?
        .dyn_into::<web_sys::Response>()
        .map_err(|_| "fetch: not a Response")?;

    let array_buffer_promise = response
        .array_buffer()
        .map_err(|e| format!("array_buffer: {:?}", e))?;
    let array_buffer = wasm_bindgen_futures::JsFuture::from(array_buffer_promise)
        .await
        .map_err(|e| format!("array_buffer: {:?}", e))?;

    let uint8 = js_sys::Uint8Array::new(&array_buffer);
    Ok(uint8.to_vec())
}

// ---------------------------------------------------------------------------
// Command dispatch
// ---------------------------------------------------------------------------

fn handle_command(state: &Rc<RefCell<WorkerState>>, cmd: RendererCommand) {
    let mut st = match state.try_borrow_mut() {
        Ok(st) => st,
        Err(_) => return,
    };

    if st.disposed {
        return;
    }

    match cmd {
        RendererCommand::LoadMesh {
            format,
            primary_url,
        } => {
            drop(st);
            handle_load_mesh(state, format, primary_url);
        }
        RendererCommand::RenderPreviewImage {
            request_id,
            format,
            primary_url,
            width,
            height,
        } => {
            drop(st);
            handle_render_preview_image(state, request_id, format, primary_url, width, height);
        }
        RendererCommand::Resize { css_w, css_h, dpr } => {
            let w = ((css_w * dpr).round() as u32).max(1);
            let h = ((css_h * dpr).round() as u32).max(1);
            if let Some(canvas) = &st.canvas {
                canvas.set_width(w);
                canvas.set_height(h);
            }
            st.pixel_width = w;
            st.pixel_height = h;
            st.needs_render = true;
        }
        RendererCommand::SetPointerState {
            x,
            y,
            buttons,
            modifiers,
            wheel_delta,
        } => {
            st.input.x = x;
            st.input.y = y;
            st.input.buttons = buttons;
            st.input.modifiers = modifiers;
            st.input.wheel_delta += wheel_delta;
            st.needs_render = true;
        }
        RendererCommand::PointerDown { x, y } => {
            st.input.button_down = true;
            st.input.last_consumed = Some((x, y));
            st.needs_render = true;
        }
        RendererCommand::PointerUp { x: _, y: _ } => {
            st.input.button_down = false;
            st.input.last_consumed = None;
            st.needs_render = true;
        }
        RendererCommand::Dispose => {
            st.disposed = true;
            if let Some(id) = st.raf_id.take() {
                let _ = st.scope.cancel_animation_frame(id);
            }
            st.raf_closure.take();
            st.renderer.take();
            st.canvas.take();
            st.scope.close();
        }
        RendererCommand::DebugCrash => {
            panic!("DebugCrash: deliberate worker crash for e2e testing");
        }
    }
}

// ---------------------------------------------------------------------------
// Canvas transfer handling
// ---------------------------------------------------------------------------

fn handle_canvas_transfer(state: &Rc<RefCell<WorkerState>>, value: &JsValue) {
    let canvas_val = js_sys::Reflect::get(value, &"__canvas".into())
        .ok()
        .filter(|v| !v.is_undefined());

    if let Some(canvas_val) = canvas_val {
        if let Ok(canvas) = canvas_val.dyn_into::<web_sys::OffscreenCanvas>() {
            let mut st = state.borrow_mut();
            if st.renderer.is_none() {
                st.init_renderer(canvas);
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------

pub fn entry() {
    console_error_panic_hook::set_once();
    tracing_wasm::set_as_global_default();

    let scope: DedicatedWorkerGlobalScope = js_sys::global().unchecked_into();
    let state = Rc::new(RefCell::new(WorkerState::new(scope.clone())));

    start_frame_loop(&state);

    let state_for_msg = state.clone();
    let onmessage = Closure::wrap(Box::new(move |event: web_sys::MessageEvent| {
        let data = event.data();

        // Canvas sentinel: transferred JS object, not postcard.
        if data.is_object() && !data.is_undefined() {
            let is_canvas = js_sys::Reflect::get(&data, &"__canvas_sentinel".into())
                .map(|v| v.is_truthy())
                .unwrap_or(false);
            if is_canvas {
                handle_canvas_transfer(&state_for_msg, &data);
                return;
            }
        }

        // String messages (handshake).
        if let Some(s) = data.as_string() {
            if s == "__worker_ready" {
                return;
            }
        }

        // Postcard bytes.
        if let Ok(array) = data.dyn_into::<js_sys::Uint8Array>() {
            let bytes = array.to_vec();
            match deserialize_command(&bytes) {
                Ok(cmd) => handle_command(&state_for_msg, cmd),
                Err(e) => {
                    tracing::error!("failed to deserialize command: {e}");
                }
            }
        }
    }) as Box<dyn FnMut(web_sys::MessageEvent)>);

    scope.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
    onmessage.forget();

    let _ = scope.post_message(&wasm_bindgen::JsValue::from_str("__worker_ready"));
}
