// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Winit example.

// The following lints are part of the Linebender standard set,
// but resolving them has been deferred for now.
// Feel free to send a PR that solves one or more of these.
#![allow(
    unreachable_pub,
    clippy::allow_attributes_without_reason,
    clippy::cast_possible_truncation
)]

use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;

use minimal_pipeline_cache::{get_cache_directory, load_pipeline_cache, write_pipeline_cache};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use vello::low_level::DebugLayers;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
use winit::keyboard::{Key, ModifiersState, NamedKey};

#[cfg(all(feature = "wgpu-profiler", not(target_arch = "wasm32")))]
use std::time::Duration;
#[cfg(all(feature = "wgpu-profiler", target_arch = "wasm32"))]
use web_time::Duration;

use clap::Parser;
use scenes::{ExampleScene, ImageCache, SceneParams, SceneSet, SimpleText};
use vello::kurbo::{Affine, Point, Vec2};
use vello::peniko::{Color, color::palette};
use vello::util::{RenderContext, RenderSurface};
use vello::{AaConfig, Renderer, RendererOptions, Scene, low_level::BumpAllocators};

use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::{Window, WindowAttributes};

use vello::wgpu::{self, CurrentSurfaceTexture, PipelineCache};

#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
mod hot_reload;
mod minimal_pipeline_cache;
mod multi_touch;
mod stats;

#[derive(Parser, Debug)]
#[command(about, long_about = None, bin_name="cargo run -p with_winit --")]
struct Args {
    /// Which scene (index) to start on
    /// Switch between scenes with left and right arrow keys
    #[arg(long)]
    scene: Option<i32>,
    #[command(flatten)]
    args: scenes::Arguments,
    #[arg(long)]
    /// Whether to use CPU shaders
    use_cpu: bool,
    /// Used to disable vsync at startup. Can be toggled with the "V" key.
    ///
    /// This setting is useful for Android, where it might be harder to press this key
    #[arg(long)]
    startup_vsync_off: bool,
    /// Used to enable gpu profiling at startup. Can be toggled with the "G" key
    ///
    /// It is off by default because it has adverse performance characteristics
    #[arg(long)]
    #[cfg(feature = "wgpu-profiler")]
    startup_gpu_profiling_on: bool,
    /// Whether to force initialising the shaders serially (rather than spawning threads)
    /// This has no effect on wasm, and defaults to 1 on macOS for performance reasons
    ///
    /// Use `0` for an automatic choice
    #[arg(long, default_value_t=default_threads())]
    num_init_threads: usize,
    /// Use the asynchronous pipeline (if available) for rendering
    ///
    /// The asynchronous pipeline is one approach for robust memory - see
    /// <https://github.com/linebender/vello/issues/366>
    ///
    /// However, it also has potential latency issues, especially for
    /// accessibility technology, as it (currently) blocks the main thread for
    /// extended periods
    #[arg(long)]
    async_pipeline: bool,
}

fn default_threads() -> usize {
    #[cfg(target_os = "macos")]
    return 1;
    #[cfg(not(target_os = "macos"))]
    return 0;
}

struct RenderState {
    surface: RenderSurface<'static>,
    valid_surface: bool,
    window: Arc<Window>,
}

#[cfg(not(target_os = "android"))]
// TODO: Make this set configurable through the command line
// Alternatively, load anti-aliasing shaders on demand/asynchronously
const AA_CONFIGS: [AaConfig; 3] = [AaConfig::Area, AaConfig::Msaa8, AaConfig::Msaa16];

#[cfg(target_os = "android")]
// Hard code to only one on Android whilst we are working on startup speed
const AA_CONFIGS: [AaConfig; 1] = [AaConfig::Area];

struct VelloApp {
    context: RenderContext,
    renderers: Vec<Option<Renderer>>,
    state: Option<RenderState>,
    // Whilst suspended, we drop `render_state`, but need to keep the same window.
    #[cfg(not(target_arch = "wasm32"))]
    cached_window: Option<Arc<Window>>,

    #[cfg(not(target_arch = "wasm32"))]
    use_cpu: bool,
    #[cfg(not(target_arch = "wasm32"))]
    num_init_threads: usize,

    scenes: Vec<ExampleScene>,
    scene: Scene,
    fragment: Scene,
    simple_text: SimpleText,
    images: ImageCache,
    stats: stats::Stats,
    stats_shown: bool,

    base_color: Option<Color>,
    async_pipeline: bool,

    scene_complexity: Option<BumpAllocators>,

    complexity_shown: bool,
    vsync_on: bool,

    #[cfg(feature = "wgpu-profiler")]
    gpu_profiling_on: bool,
    #[cfg(feature = "wgpu-profiler")]
    profile_stored: Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
    #[cfg(feature = "wgpu-profiler")]
    profile_taken: Instant,

    // We allow cycling through AA configs in either direction, so use a signed index
    aa_config_ix: i32,

    frame_start_time: Instant,
    start: Instant,

    touch_state: multi_touch::TouchState,
    // navigation_fingers are fingers which are used in the navigation 'zone' at the bottom
    // of the screen. This ensures that one press on the screen doesn't have multiple actions
    navigation_fingers: HashSet<u64>,
    transform: Affine,
    mouse_down: bool,
    prior_position: Option<Point>,
    // We allow looping left and right through the scenes, so use a signed index
    scene_ix: i32,
    complexity: usize,

    prev_scene_ix: i32,
    modifiers: ModifiersState,

    debug: DebugLayers,

    #[cfg(not(target_arch = "wasm32"))]
    cache_data: Option<(PathBuf, std::sync::mpsc::Sender<(PipelineCache, PathBuf)>)>,
}

impl ApplicationHandler<UserEvent> for VelloApp {
    #[cfg(target_arch = "wasm32")]
    fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}

    #[cfg(not(target_arch = "wasm32"))]
    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
        let None = self.state else {
            return;
        };
        let window = self
            .cached_window
            .take()
            .unwrap_or_else(|| Arc::new(event_loop.create_window(window_attributes()).unwrap()));
        let size = window.inner_size();
        let present_mode = if self.vsync_on {
            wgpu::PresentMode::AutoVsync
        } else {
            wgpu::PresentMode::AutoNoVsync
        };
        let surface_future =
            self.context
                .create_surface(window.clone(), size.width, size.height, present_mode);
        // We need to block here, in case a Suspended event appeared
        let surface = pollster::block_on(surface_future).expect("Error creating surface");
        self.state = {
            let render_state = RenderState {
                window,
                surface,
                valid_surface: true,
            };
            self.renderers
                .resize_with(self.context.devices.len(), || None);
            let id = render_state.surface.dev_id;
            self.renderers[id].get_or_insert_with(|| {
                let device_handle = &self.context.devices[id];
                let cache = if let Some((dir, tx)) = self.cache_data.as_ref() {
                    // Safety: Hoping for the best. Given that we're using as private a cache directory as possible, it's
                    // probably fine?
                    unsafe {
                        load_pipeline_cache(
                            &device_handle.device,
                            &device_handle.adapter().get_info(),
                            dir,
                        )
                        .unwrap()
                        .map(|(cache, file)| (cache, file, tx.clone()))
                    }
                } else {
                    None
                };
                let start = Instant::now();
                let renderer = Renderer::new(
                    &device_handle.device,
                    RendererOptions {
                        use_cpu: self.use_cpu,
                        antialiasing_support: AA_CONFIGS.iter().copied().collect(),
                        num_init_threads: NonZeroUsize::new(self.num_init_threads),
                        pipeline_cache: cache.as_ref().map(|(cache, _, _)| cache.clone()),
                    },
                )
                .map_err(|e| {
                    // Pretty-print any renderer creation error using Display formatting before unwrapping.
                    anyhow::format_err!("{e}")
                })
                .expect("Failed to create renderer");
                log::info!("Creating renderer {id} took {:?}", start.elapsed());
                #[cfg(feature = "wgpu-profiler")]
                let mut renderer = renderer;
                #[cfg(feature = "wgpu-profiler")]
                renderer
                    .profiler
                    .change_settings(wgpu_profiler::GpuProfilerSettings {
                        enable_timer_queries: self.gpu_profiling_on,
                        enable_debug_groups: self.gpu_profiling_on,
                        ..Default::default()
                    })
                    .expect("Not setting max_num_pending_frames");
                if let Some((cache, file, tx)) = cache {
                    drop(tx.send((cache, file)));
                }
                renderer
            });
            Some(render_state)
        };
    }

    fn window_event(
        &mut self,
        event_loop: &winit::event_loop::ActiveEventLoop,
        window_id: winit::window::WindowId,
        event: WindowEvent,
    ) {
        let Some(render_state) = &mut self.state else {
            return;
        };
        if render_state.window.id() != window_id {
            return;
        }
        let _span = if !matches!(event, WindowEvent::RedrawRequested) {
            Some(tracing::trace_span!("Handling window event", ?event).entered())
        } else {
            None
        };
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
                match event.logical_key.as_ref() {
                    Key::Named(NamedKey::ArrowLeft) => {
                        self.scene_ix = self.scene_ix.saturating_sub(1);
                    }
                    Key::Named(NamedKey::ArrowRight) => {
                        self.scene_ix = self.scene_ix.saturating_add(1);
                    }
                    Key::Named(NamedKey::ArrowUp) => self.complexity += 1,
                    Key::Named(NamedKey::ArrowDown) => {
                        self.complexity = self.complexity.saturating_sub(1);
                    }
                    Key::Named(NamedKey::Space) => {
                        self.transform = Affine::IDENTITY;
                    }
                    Key::Character(char) => {
                        // TODO: Have a more principled way of handling modifiers on keypress
                        // see e.g. https://xi.zulipchat.com/#narrow/channel/351333-glazier/topic/Keyboard.20shortcuts/with/403538769
                        let char = char.to_lowercase();
                        match char.as_str() {
                            "q" | "e" => {
                                if let Some(prior_position) = self.prior_position {
                                    let is_clockwise = char == "e";
                                    let angle = if is_clockwise { -0.05 } else { 0.05 };
                                    self.transform =
                                        self.transform.then_rotate_about(angle, prior_position);
                                }
                            }
                            "s" => {
                                self.stats_shown = !self.stats_shown;
                            }
                            "d" => {
                                self.complexity_shown = !self.complexity_shown;
                            }
                            "c" => {
                                self.stats.clear_min_and_max();
                            }
                            "m" => {
                                self.aa_config_ix = if self.modifiers.shift_key() {
                                    self.aa_config_ix.saturating_sub(1)
                                } else {
                                    self.aa_config_ix.saturating_add(1)
                                };
                            }
                            #[cfg(feature = "wgpu-profiler")]
                            "p" => {
                                if let Some(renderer) = &self.renderers[render_state.surface.dev_id]
                                {
                                    store_profiling(renderer, &self.profile_stored);
                                }
                            }
                            #[cfg(feature = "wgpu-profiler")]
                            "g" => {
                                self.gpu_profiling_on = !self.gpu_profiling_on;
                                if let Some(renderer) =
                                    &mut self.renderers[render_state.surface.dev_id]
                                {
                                    renderer
                                        .profiler
                                        .change_settings(wgpu_profiler::GpuProfilerSettings {
                                            enable_timer_queries: self.gpu_profiling_on,
                                            enable_debug_groups: self.gpu_profiling_on,
                                            ..Default::default()
                                        })
                                        .expect("Not setting max_num_pending_frames");
                                }
                            }
                            "v" => {
                                self.vsync_on = !self.vsync_on;
                                self.context.set_present_mode(
                                    &mut render_state.surface,
                                    if self.vsync_on {
                                        wgpu::PresentMode::AutoVsync
                                    } else {
                                        wgpu::PresentMode::AutoNoVsync
                                    },
                                );
                            }
                            debug_layer @ ("1" | "2" | "3" | "4") => {
                                match debug_layer {
                                    "1" => {
                                        self.debug.toggle(DebugLayers::BOUNDING_BOXES);
                                    }
                                    "2" => {
                                        self.debug.toggle(DebugLayers::LINESOUP_SEGMENTS);
                                    }
                                    "3" => {
                                        self.debug.toggle(DebugLayers::LINESOUP_POINTS);
                                    }
                                    "4" => {
                                        self.debug.toggle(DebugLayers::VALIDATION);
                                    }
                                    _ => unreachable!(),
                                }
                                if !self.debug.is_empty() && !self.async_pipeline {
                                    log::warn!(
                                        "Debug Layers won't work without using `--async-pipeline`. Requested {:?}",
                                        self.debug
                                    );
                                }
                            }
                            _ => {}
                        }
                    }
                    Key::Named(NamedKey::Escape) => event_loop.exit(),
                    _ => {}
                }
            }
            WindowEvent::Touch(touch) => {
                match touch.phase {
                    TouchPhase::Started => {
                        // We reserve the bottom third of the screen for navigation
                        // This also prevents strange effects whilst using the navigation gestures on Android
                        // TODO: How do we know what the client area is? Winit seems to just give us the
                        // full screen
                        // TODO: Render a display of the navigation regions. We don't do
                        // this currently because we haven't researched how to determine when we're
                        // in a touch context (i.e. Windows/Linux/MacOS with a touch screen could
                        // also be using mouse/keyboard controls)
                        // Note that winit's rendering is y-down
                        if let Some(RenderState {
                            surface,
                            valid_surface,
                            ..
                        }) = &self.state
                            && *valid_surface
                            && touch.location.y > surface.config.height as f64 * 2. / 3.
                        {
                            self.navigation_fingers.insert(touch.id);
                            // The left third of the navigation zone navigates backwards
                            if touch.location.x < surface.config.width as f64 / 3. {
                                self.scene_ix = self.scene_ix.saturating_sub(1);
                            } else if touch.location.x > 2. * surface.config.width as f64 / 3. {
                                self.scene_ix = self.scene_ix.saturating_add(1);
                            }
                        }
                    }
                    TouchPhase::Ended | TouchPhase::Cancelled => {
                        // We intentionally ignore the result here
                        self.navigation_fingers.remove(&touch.id);
                    }
                    TouchPhase::Moved => (),
                }
                // See documentation on navigation_fingers
                if !self.navigation_fingers.contains(&touch.id) {
                    self.touch_state.add_event(&touch);
                }
            }
            WindowEvent::Resized(size) => {
                if let Some(RenderState {
                    surface,
                    valid_surface,
                    window,
                }) = &mut self.state
                {
                    if size.width != 0 && size.height != 0 {
                        self.context
                            .resize_surface(surface, size.width, size.height);
                        *valid_surface = true;
                        window.request_redraw();
                    } else {
                        *valid_surface = false;
                    }
                }
            }
            WindowEvent::MouseInput {
                state,
                button: MouseButton::Left,
                ..
            } => {
                self.mouse_down = state == ElementState::Pressed;
            }
            WindowEvent::MouseWheel { delta, .. } => {
                const BASE: f64 = 1.05;
                const PIXELS_PER_LINE: f64 = 20.0;

                if let Some(prior_position) = self.prior_position {
                    let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta {
                        delta.y / PIXELS_PER_LINE
                    } else if let MouseScrollDelta::LineDelta(_, y) = delta {
                        y as f64
                    } else {
                        0.0
                    };
                    self.transform = self
                        .transform
                        .then_scale_about(BASE.powf(exponent), prior_position);
                } else {
                    log::warn!("Scrolling without mouse in window; this shouldn't be possible");
                }
            }
            WindowEvent::CursorLeft { .. } => {
                self.prior_position = None;
            }
            WindowEvent::CursorMoved { position, .. } => {
                let position = Point {
                    x: position.x,
                    y: position.y,
                };
                if self.mouse_down
                    && let Some(prior) = self.prior_position
                {
                    self.transform = self.transform.then_translate(position - prior);
                }
                self.prior_position = Some(position);
            }
            WindowEvent::RedrawRequested => {
                let _rendering_span = tracing::trace_span!("Actioning Requested Redraw").entered();
                let encoding_span = tracing::trace_span!("Encoding scene").entered();

                render_state.window.request_redraw();

                let Some(RenderState {
                    surface,
                    valid_surface,
                    window,
                }) = &self.state
                else {
                    return;
                };
                if !valid_surface {
                    return;
                }
                let width = surface.config.width;
                let height = surface.config.height;
                let device_handle = &self.context.devices[surface.dev_id];
                let snapshot = self.stats.snapshot();

                // Allow looping forever
                self.scene_ix = self.scene_ix.rem_euclid(self.scenes.len() as i32);
                self.aa_config_ix = self.aa_config_ix.rem_euclid(AA_CONFIGS.len() as i32);

                let example_scene = &mut self.scenes[self.scene_ix as usize];
                if self.prev_scene_ix != self.scene_ix {
                    self.transform = Affine::IDENTITY;
                    self.prev_scene_ix = self.scene_ix;
                    window.set_title(&format!("Vello demo - {}", example_scene.config.name));
                }
                self.fragment.reset();
                let mut scene_params = SceneParams {
                    time: self.start.elapsed().as_secs_f64(),
                    text: &mut self.simple_text,
                    images: &mut self.images,
                    resolution: None,
                    base_color: None,
                    interactive: true,
                    complexity: self.complexity,
                };
                example_scene
                    .function
                    .render(&mut self.fragment, &mut scene_params);

                // If the user specifies a base color in the CLI we use that. Otherwise we use any
                // color specified by the scene. The default is black.
                let base_color = self
                    .base_color
                    .or(scene_params.base_color)
                    .unwrap_or(palette::css::BLACK);
                let antialiasing_method = AA_CONFIGS[self.aa_config_ix as usize];
                let render_params = vello::RenderParams {
                    base_color,
                    width,
                    height,
                    antialiasing_method,
                };
                self.scene.reset();
                let mut transform = self.transform;
                if let Some(resolution) = scene_params.resolution {
                    // Automatically scale the rendering to fill as much of the window as possible
                    // TODO: Apply svg view_box, somehow
                    let scale_factor =
                        (width as f64 / resolution.x).min(height as f64 / resolution.y);
                    transform *= Affine::scale(scale_factor);
                }
                self.scene.append(&self.fragment, Some(transform));
                if self.stats_shown {
                    snapshot.draw_layer(
                        &mut self.scene,
                        scene_params.text,
                        width as f64,
                        height as f64,
                        self.stats.samples(),
                        self.complexity_shown
                            .then_some(self.scene_complexity)
                            .flatten(),
                        self.vsync_on,
                        antialiasing_method,
                    );
                    #[cfg(feature = "wgpu-profiler")]
                    if let Some(profiling_result) = self.renderers[surface.dev_id]
                        .as_mut()
                        .and_then(|renderer| renderer.profile_result.take())
                        && (self.profile_stored.is_none()
                            || self.profile_taken.elapsed() > Duration::from_secs(1))
                    {
                        self.profile_stored = Some(profiling_result);
                        self.profile_taken = Instant::now();
                    }
                    #[cfg(feature = "wgpu-profiler")]
                    if let Some(profiling_result) = self.profile_stored.as_ref() {
                        stats::draw_gpu_profiling(
                            &mut self.scene,
                            scene_params.text,
                            width as f64,
                            height as f64,
                            profiling_result,
                        );
                    }
                }
                drop(encoding_span);
                let render_span = tracing::trace_span!("Dispatching render").entered();
                // Note: we don't run the async/"robust" pipeline on web, as
                // it requires more async wiring for the readback. See
                // [#vello > async on wasm](https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/async.20on.20wasm/with/396685264)
                #[expect(
                    deprecated,
                    reason = "We still want to use the async pipeline for the debug layers"
                )]
                if self.async_pipeline && cfg!(not(target_arch = "wasm32")) {
                    self.scene_complexity = vello::util::block_on_wgpu(
                        &device_handle.device,
                        self.renderers[surface.dev_id]
                            .as_mut()
                            .unwrap()
                            .render_to_texture_async(
                                &device_handle.device,
                                &device_handle.queue,
                                &self.scene,
                                &surface.target_view,
                                &render_params,
                                self.debug,
                            ),
                    )
                    .expect("failed to render to texture");
                } else {
                    self.renderers[surface.dev_id]
                        .as_mut()
                        .unwrap()
                        .render_to_texture(
                            &device_handle.device,
                            &device_handle.queue,
                            &self.scene,
                            &surface.target_view,
                            &render_params,
                        )
                        .expect("failed to render to texture");
                }
                drop(render_span);

                let texture_span = tracing::trace_span!("Blitting to surface").entered();
                let surface_texture = match surface.surface.get_current_texture() {
                    CurrentSurfaceTexture::Success(surface_texture) => surface_texture,
                    CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
                        self.context.configure_surface(surface);
                        window.request_redraw();
                        return;
                    }
                    CurrentSurfaceTexture::Occluded | CurrentSurfaceTexture::Timeout => {
                        window.request_redraw();
                        return;
                    }
                    CurrentSurfaceTexture::Lost => panic!("Surface was lost"),
                    CurrentSurfaceTexture::Validation => {
                        panic!("Validation error getting surface")
                    }
                };
                // Perform the copy
                // (TODO: Does it improve throughput to acquire the surface after the previous texture render has happened?)
                let mut encoder =
                    device_handle
                        .device
                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                            label: Some("Surface Blit"),
                        });
                surface.blitter.copy(
                    &device_handle.device,
                    &mut encoder,
                    &surface.target_view,
                    &surface_texture
                        .texture
                        .create_view(&wgpu::TextureViewDescriptor::default()),
                );
                device_handle.queue.submit([encoder.finish()]);
                surface_texture.present();
                drop(texture_span);

                {
                    let _poll_span = tracing::trace_span!("Polling wgpu device").entered();
                    device_handle.device.poll(wgpu::PollType::Poll).unwrap();
                }
                let new_time = Instant::now();
                self.stats.add_sample(stats::Sample {
                    frame_time_us: (new_time - self.frame_start_time).as_micros() as u64,
                });
                self.frame_start_time = new_time;
            }
            _ => {}
        }
    }

    fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
        self.touch_state.end_frame();
        let touch_info = self.touch_state.info();
        if let Some(touch_info) = touch_info {
            let centre = Vec2::new(touch_info.zoom_centre.x, touch_info.zoom_centre.y);
            self.transform = Affine::translate(touch_info.translation_delta)
                * Affine::translate(centre)
                * Affine::scale(touch_info.zoom_delta)
                * Affine::rotate(touch_info.rotation_delta)
                * Affine::translate(-centre)
                * self.transform;
        }

        if let Some(render_state) = &mut self.state {
            render_state.window.request_redraw();
        }
    }

    fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) {
        match event {
            #[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
            UserEvent::HotReload => {
                let Some(render_state) = &mut self.state else {
                    return;
                };
                let device_handle = &self.context.devices[render_state.surface.dev_id];
                log::info!("==============\nReloading shaders");
                let start = Instant::now();
                let result = self.renderers[render_state.surface.dev_id]
                    .as_mut()
                    .unwrap()
                    .reload_shaders(&device_handle.device);
                // We know that the only async here (`pop_error_scope`) is actually sync, so blocking is fine
                match pollster::block_on(result) {
                    Ok(_) => log::info!("Reloading took {:?}", start.elapsed()),
                    Err(e) => log::error!("Failed to reload shaders: {e}"),
                }
            }
        }
    }

    fn suspended(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
        log::info!("Suspending");
        #[cfg(not(target_arch = "wasm32"))]
        // When we suspend, we need to remove the `wgpu` Surface
        if let Some(render_state) = self.state.take() {
            self.cached_window = Some(render_state.window);
        }
    }
}

fn run(
    event_loop: EventLoop<UserEvent>,
    args: Args,
    scenes: SceneSet,
    render_cx: RenderContext,
    #[cfg(target_arch = "wasm32")] render_state: RenderState,
) {
    use winit::keyboard::ModifiersState;

    #[cfg(not(target_arch = "wasm32"))]
    let (render_state, renderers) = (None::<RenderState>, vec![]);

    let cache_directory = get_cache_directory(&event_loop).unwrap();
    // The design of `RenderContext` forces delayed renderer initialisation to
    // not work on wasm, as WASM futures effectively must be 'static.
    // Otherwise, this could work by sending the result to event_loop.proxy
    // instead of blocking
    #[cfg(target_arch = "wasm32")]
    let (render_state, renderers) = {
        let mut renderers = vec![];
        renderers.resize_with(render_cx.devices.len(), || None);
        let id = render_state.surface.dev_id;
        let device_handle = &render_cx.devices[id];
        let cache: Option<(PipelineCache, PathBuf)> = if let Some(dir) = cache_directory.as_ref() {
            // Safety: Hoping for the best. Given that we're using as private a cache directory as possible, it's
            // probably fine?
            unsafe {
                load_pipeline_cache(
                    &device_handle.device,
                    &device_handle.adapter().get_info(),
                    dir,
                )
                .unwrap()
            }
        } else {
            None
        };
        let renderer = Renderer::new(
            &device_handle.device,
            RendererOptions {
                use_cpu: args.use_cpu,
                antialiasing_support: AA_CONFIGS.iter().copied().collect(),
                // We currently initialise on one thread on WASM, but mark this here
                // anyway
                num_init_threads: NonZeroUsize::new(1),
                pipeline_cache: cache.as_ref().map(|(cache, _)| cache.clone()),
            },
        )
        .map_err(|e| {
            // Pretty-print any renderer creation error using Display formatting before unwrapping.
            eprintln!("{e}");
            e
        })
        .expect("Failed to create renderer");
        #[cfg(feature = "wgpu-profiler")]
        let mut renderer = renderer;
        #[cfg(feature = "wgpu-profiler")]
        renderer
            .profiler
            .change_settings(wgpu_profiler::GpuProfilerSettings {
                enable_timer_queries: args.startup_gpu_profiling_on,
                enable_debug_groups: args.startup_gpu_profiling_on,
                ..Default::default()
            })
            .expect("Not setting max_num_pending_frames");
        renderers[id] = Some(renderer);
        if let Some((cache, file)) = cache {
            let result = write_pipeline_cache(&file, &cache);
            if let Err(e) = result {
                log::error!("Failed to write pipeline cache: {e}");
            }
        }
        (Some(render_state), renderers)
    };
    #[cfg(not(target_arch = "wasm32"))]
    let cache_data = if let Some(cache_directory) = cache_directory {
        let (tx, rx) = std::sync::mpsc::channel::<(PipelineCache, PathBuf)>();
        std::thread::spawn(move || {
            while let Ok((cache, path)) = rx.recv() {
                let result = write_pipeline_cache(&path, &cache);
                if let Err(e) = result {
                    log::error!("Failed to write pipeline cache: {e}");
                }
            }
        });
        Some((cache_directory, tx))
    } else {
        None
    };
    let debug = DebugLayers::none();

    let mut app = VelloApp {
        context: render_cx,
        renderers,
        state: render_state,
        #[cfg(not(target_arch = "wasm32"))]
        cached_window: None,
        #[cfg(not(target_arch = "wasm32"))]
        use_cpu: args.use_cpu,
        #[cfg(not(target_arch = "wasm32"))]
        num_init_threads: args.num_init_threads,
        scenes: scenes.scenes,
        scene: Scene::new(),
        fragment: Scene::new(),
        simple_text: SimpleText::new(),
        images: ImageCache::new(),
        stats: stats::Stats::new(),
        stats_shown: true,
        base_color: args.args.base_color,
        async_pipeline: args.async_pipeline,
        scene_complexity: None,
        complexity_shown: false,
        vsync_on: !args.startup_vsync_off,

        #[cfg(feature = "wgpu-profiler")]
        gpu_profiling_on: args.startup_gpu_profiling_on,
        #[cfg(feature = "wgpu-profiler")]
        profile_stored: None,
        #[cfg(feature = "wgpu-profiler")]
        profile_taken: Instant::now(),

        aa_config_ix: 0,

        frame_start_time: Instant::now(),
        start: Instant::now(),

        touch_state: multi_touch::TouchState::new(),
        navigation_fingers: HashSet::new(),
        transform: Affine::IDENTITY,
        mouse_down: false,
        prior_position: None,
        scene_ix: args.scene.unwrap_or(0),
        complexity: 0,
        prev_scene_ix: 0,
        modifiers: ModifiersState::default(),
        debug,
        #[cfg(not(target_arch = "wasm32"))]
        cache_data,
    };

    event_loop.run_app(&mut app).expect("run to completion");
}

#[cfg(feature = "wgpu-profiler")]
/// A function extracted to fix rustfmt
fn store_profiling(
    renderer: &Renderer,
    profile_stored: &Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
) {
    if let Some(profile_result) = &renderer.profile_result.as_ref().or(profile_stored.as_ref()) {
        // There can be empty results if the required features aren't supported
        if !profile_result.is_empty() {
            let path = std::path::Path::new("trace.json");
            match wgpu_profiler::chrometrace::write_chrometrace(path, profile_result) {
                Ok(()) => {
                    println!("Wrote trace to path {path:?}");
                }
                Err(e) => {
                    log::warn!("Failed to write trace {e}");
                }
            }
        }
    }
}

fn window_attributes() -> WindowAttributes {
    Window::default_attributes()
        .with_inner_size(LogicalSize::new(1044, 800))
        .with_resizable(true)
        .with_title("Vello demo")
}

#[derive(Debug)]
enum UserEvent {
    #[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
    HotReload,
}

#[cfg(target_arch = "wasm32")]
fn display_error_message() -> Option<()> {
    let window = web_sys::window()?;
    let document = window.document()?;
    let elements = document.get_elements_by_tag_name("body");
    let body = elements.item(0)?;
    body.set_inner_html(
        r#"<style>
        p {
            margin: 2em 10em;
            font-family: sans-serif;
        }
        </style>
        <p><a href="https://caniuse.com/webgpu">WebGPU</a>
        is not enabled. Make sure your browser is updated to
        <a href="https://chromiumdash.appspot.com/schedule">Chrome M113</a> or
        another browser compatible with WebGPU.</p>"#,
    );
    Some(())
}

/// Entry point.
#[cfg(not(target_os = "android"))]
pub fn main() -> anyhow::Result<()> {
    // TODO: initializing both env_logger and console_logger fails on wasm.
    // Figure out a more principled approach.
    #[cfg(not(target_arch = "wasm32"))]
    env_logger::builder()
        .format_timestamp(Some(env_logger::TimestampPrecision::Millis))
        .filter_level(log::LevelFilter::Warn)
        .init();
    let args = parse_arguments();
    let scenes = args.args.select_scene_set()?;
    if let Some(scenes) = scenes {
        let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
        let render_cx = RenderContext::new();
        #[cfg(not(target_arch = "wasm32"))]
        {
            let proxy = event_loop.create_proxy();
            let _keep = hot_reload::hot_reload(move || {
                proxy.send_event(UserEvent::HotReload).ok().map(drop)
            });

            run(event_loop, args, scenes, render_cx);
        }
        #[cfg(target_arch = "wasm32")]
        {
            let mut render_cx = render_cx;
            std::panic::set_hook(Box::new(console_error_panic_hook::hook));
            console_log::init().expect("could not initialize logger");
            use winit::platform::web::WindowExtWebSys;
            #[allow(deprecated)]
            let window = Arc::new(event_loop.create_window(window_attributes()).unwrap());
            // On wasm, append the canvas to the document body
            let canvas = window.canvas().unwrap();
            web_sys::window()
                .and_then(|win| win.document())
                .and_then(|doc| doc.body())
                .and_then(|body| body.append_child(canvas.as_ref()).ok())
                .expect("couldn't append canvas to document body");
            // Best effort to start with the canvas focused, taking input
            drop(web_sys::HtmlElement::from(canvas).focus());
            wasm_bindgen_futures::spawn_local(async move {
                let (width, height, scale_factor) = web_sys::window()
                    .map(|w| {
                        (
                            w.inner_width().unwrap().as_f64().unwrap(),
                            w.inner_height().unwrap().as_f64().unwrap(),
                            w.device_pixel_ratio(),
                        )
                    })
                    .unwrap();
                let size =
                    winit::dpi::PhysicalSize::from_logical::<_, f64>((width, height), scale_factor);
                _ = window.request_inner_size(size);
                let surface = render_cx
                    .create_surface(
                        window.clone(),
                        size.width,
                        size.height,
                        wgpu::PresentMode::AutoVsync,
                    )
                    .await;
                if let Ok(surface) = surface {
                    let render_state = RenderState {
                        window,
                        surface,
                        valid_surface: true,
                    };
                    // No error handling here; if the event loop has finished, we don't need to send them the surface
                    run(event_loop, args, scenes, render_cx, render_state);
                } else {
                    _ = display_error_message();
                }
            });
        }
    }
    Ok(())
}

fn parse_arguments() -> Args {
    // We allow baking in arguments at compile time. This is especially useful for
    // Android and WASM.
    // This is used on desktop platforms to allow debugging the same settings
    if let Some(args) = option_env!("VELLO_STATIC_ARGS") {
        // We split by whitespace here to allow passing multiple arguments
        // In theory, we could do more advanced parsing/splitting (e.g. using quotes),
        // but that would require a lot more effort

        // We `chain` in a fake binary name, because clap ignores the first argument otherwise
        // Ideally, we'd use the `no_binary_name` argument, but setting that at runtime would
        // require globals or some worse hacks
        Args::parse_from(std::iter::once("with_winit").chain(args.split_ascii_whitespace()))
    } else {
        Args::parse()
    }
}

#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;

#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(app: AndroidApp) {
    use winit::platform::android::EventLoopBuilderExtAndroid;
    let config = android_logger::Config::default();
    // We allow configuring the Android logging with an environment variable at build time
    let config = if let Some(logging_config) = option_env!("VELLO_STATIC_LOG") {
        let mut filter = android_logger::FilterBuilder::new();
        filter.filter_level(log::LevelFilter::Warn);
        filter.parse(logging_config);
        let filter = filter.build();
        // This shouldn't be needed in theory, but without this the max
        // level is set to 0 (i.e. Off)
        let config = config.with_max_level(filter.filter());
        config.with_filter(filter)
    } else {
        config.with_max_level(log::LevelFilter::Warn)
    };
    android_logger::init_once(config);

    // Send tracing events to Android Trace
    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

    tracing_subscriber::registry()
        .with(tracing_android_trace::AndroidTraceLayer::new())
        .try_init()
        .unwrap();
    log::info!(
        "Max level: {}",
        tracing::level_filters::LevelFilter::current()
    );

    let event_loop = EventLoop::with_user_event()
        .with_android_app(app)
        .build()
        .expect("Required to continue");
    let args = parse_arguments();
    let scenes = args.args.select_scene_set().unwrap().unwrap();
    let render_cx = RenderContext::new();

    run(event_loop, args, scenes, render_cx);
}

#[cfg(all(feature = "_ci_dep_features_to_test", test))]
#[test]
// This just tests that the "kurbo" dependency we enable schemars for
// aligns to the same version that vello's peniko dependency resolves to.
fn test_kurbo_schemars_with_peniko() {
    use std::marker::PhantomData;
    #[expect(unused_qualifications)]
    let _: PhantomData<kurbo::Rect> = PhantomData::<vello::peniko::kurbo::Rect>;
}
