pub mod animation_utils;
pub mod box_drawing;
pub mod cursor_renderer;
pub mod fonts;
pub mod grid_renderer;
pub mod opengl;
pub mod profiler;
pub mod progress_bar;
mod rendered_layer;
pub mod rendered_window;
mod vsync;

#[cfg(target_os = "windows")]
pub mod d3d;

#[cfg(target_os = "macos")]
mod metal;

use std::{
    cmp::Ordering,
    collections::{HashMap, hash_map::Entry},
    rc::Rc,
    sync::Arc,
};

use itertools::Itertools;
use progress_bar::{ProgressBar, ProgressBarSettings};
use skia_safe::{Canvas, Color4f, Paint};

use winit::{
    event::WindowEvent,
    event_loop::{ActiveEventLoop, EventLoopProxy},
    window::{Window, WindowAttributes},
};

use crate::{
    WindowSettings,
    bridge::EditorMode,
    cmd_line::CmdLineSettings,
    editor::{Cursor, Style, WindowType},
    profiling::{tracy_create_gpu_context, tracy_named_frame, tracy_zone},
    renderer::{
        fonts::font_options::PixelGeometry,
        rendered_layer::{FloatingLayer, group_windows},
    },
    settings::*,
    units::{GridPos, GridRect, GridScale, GridSize, PixelPos, PixelRect, to_skia_rect},
    window::{EventPayload, ShouldRender},
};

#[cfg(feature = "profiling")]
use crate::profiling::tracy_plot;
#[cfg(feature = "profiling")]
use skia_safe::graphics::{
    font_cache_count_limit, font_cache_count_used, font_cache_limit, font_cache_used,
    resource_cache_single_allocation_byte_limit, resource_cache_total_bytes_limit,
    resource_cache_total_bytes_used,
};

#[cfg(feature = "gpu_profiling")]
use crate::profiling::GpuCtx;

use cursor_renderer::CursorRenderer;
pub use fonts::caching_shaper::CachingShaper;
pub use grid_renderer::GridRenderer;
pub use rendered_window::{RenderedWindow, WindowDrawCommand, WindowDrawDetails};

pub use vsync::VSync;

use self::fonts::font_options::FontOptions;

const MESSAGE_SELECTION_ALPHA: f32 = 0.35;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct MessageSelection {
    pub grid_id: u64,
    pub start: GridPos<u32>,
    pub end: GridPos<u32>,
}

#[cfg(feature = "profiling")]
fn plot_skia_cache() {
    tracy_plot!("font_cache_limit", font_cache_limit() as f64);
    tracy_plot!("font_cache_used", font_cache_used() as f64);
    tracy_plot!("font_cache_count_used", font_cache_count_used() as f64);
    tracy_plot!("font_cache_count_limit", font_cache_count_limit() as f64);
    tracy_plot!("resource_cache_total_bytes_used", resource_cache_total_bytes_used() as f64);
    tracy_plot!("resource_cache_total_bytes_limit", resource_cache_total_bytes_limit() as f64);
    tracy_plot!(
        "resource_cache_single_allocation_byte_limit",
        resource_cache_single_allocation_byte_limit().unwrap_or_default() as f64
    );
}

#[derive(SettingGroup, Clone)]
pub struct RendererSettings {
    position_animation_length: f32,
    scroll_animation_length: f32,
    scroll_animation_far_lines: u32,
    floating_blur: bool,
    floating_blur_amount_x: f32,
    floating_blur_amount_y: f32,
    floating_shadow: bool,
    floating_z_height: f32,
    floating_corner_radius: f32,
    light_angle_degrees: f32,
    light_radius: f32,
    debug_renderer: bool,
    profiler: bool,
    underline_stroke_scale: f32,
    text_gamma: f32,
    text_contrast: f32,
    experimental_layer_grouping: bool,
    pixel_geometry: PixelGeometry,
}

impl Default for RendererSettings {
    fn default() -> Self {
        Self {
            position_animation_length: 0.15,
            scroll_animation_length: 0.3,
            scroll_animation_far_lines: 1,
            floating_blur: true,
            floating_blur_amount_x: 2.0,
            floating_blur_amount_y: 2.0,
            floating_shadow: true,
            floating_z_height: 10.,
            floating_corner_radius: 0.0,
            light_angle_degrees: 45.,
            light_radius: 5.,
            debug_renderer: false,
            profiler: false,
            underline_stroke_scale: 1.,
            text_gamma: 0.0,
            text_contrast: 0.5,
            experimental_layer_grouping: false,
            pixel_geometry: PixelGeometry::default(),
        }
    }
}

// Since draw commmands are inserted into a heap, we need to implement Ord such that
// the commands that should be processed first (such as window draw commands or close
// window) are sorted as larger than the ones that should be handled later
// So the order of the variants here matters so that the derive implementation can get
// the order in the binary heap correct
#[derive(Debug, Clone, PartialEq)]
pub enum DrawCommand {
    UpdateCursor(Cursor),
    FontChanged(String),
    LineSpaceChanged(f32),
    DefaultStyleChanged(Style),
    ModeChanged(EditorMode),
    UIReady,
    Window { grid_id: u64, command: WindowDrawCommand },
}

pub struct Renderer {
    cursor_renderer: CursorRenderer,
    pub grid_renderer: GridRenderer,
    current_mode: EditorMode,

    pub progress_bar: ProgressBar,

    pub rendered_windows: HashMap<u64, RenderedWindow>,
    pub window_regions: Vec<WindowDrawDetails>,

    profiler: profiler::Profiler,
    pub os_scale_factor: f64,
    pub user_scale_factor: f64,

    settings: Arc<Settings>,
    message_selection: Option<MessageSelection>,
}

/// Results of processing the draw commands from the command channel.
pub struct DrawCommandResult {
    pub font_changed: bool,
    pub should_show: bool,
}

impl Renderer {
    pub fn new(os_scale_factor: f64, init_config: Config, settings: Arc<Settings>) -> Self {
        let window_settings = settings.get::<WindowSettings>();

        let user_scale_factor = window_settings.scale_factor.into();
        let scale_factor = user_scale_factor * os_scale_factor;
        let cursor_renderer = CursorRenderer::new(settings.clone());
        let mut grid_renderer = GridRenderer::new(scale_factor, settings.clone());
        let mut font_config_state = settings.get::<FontConfigState>();
        font_config_state.has_font = init_config.font.is_some();
        settings.set(&font_config_state);
        grid_renderer.update_font_options(init_config.font.map(|x| x.into()).unwrap_or_default());
        grid_renderer.handle_box_drawing_update(init_config.box_drawing.unwrap_or_default());
        let current_mode = EditorMode::Unknown(String::from(""));

        let rendered_windows = HashMap::new();
        let window_regions = Vec::new();

        let profiler = profiler::Profiler::new(12.0, settings.clone());

        let progress_bar = ProgressBar::new();

        Renderer {
            rendered_windows,
            cursor_renderer,
            grid_renderer,
            current_mode,
            window_regions,
            profiler,
            progress_bar,
            os_scale_factor,
            user_scale_factor,
            settings,
            message_selection: None,
        }
    }

    pub fn handle_event(&mut self, event: &WindowEvent) {
        self.cursor_renderer.handle_event(event);
    }

    pub fn font_names(&self) -> Vec<String> {
        self.grid_renderer.font_names()
    }

    pub fn set_message_selection(&mut self, selection: Option<MessageSelection>) {
        self.message_selection = selection;
    }

    pub fn prepare_frame(&mut self) -> ShouldRender {
        self.cursor_renderer.prepare_frame()
    }

    pub fn draw_frame(
        &mut self,
        root_canvas: &Canvas,
        content_region: Option<&PixelRect<f32>>,
        dt: f32,
    ) {
        tracy_zone!("renderer_draw_frame");
        let window_settings = self.settings.get::<WindowSettings>();
        let opacity = if window_settings.normal_opacity < 1.0 {
            window_settings.normal_opacity
        } else {
            window_settings.opacity
        };
        let default_background = self.grid_renderer.get_default_background(opacity);
        let grid_scale = self.grid_renderer.grid_scale;

        let layer_grouping = self.settings.get::<RendererSettings>().experimental_layer_grouping;
        root_canvas.clear(default_background);
        root_canvas.save();
        root_canvas.reset_matrix();

        if let Some(content_region) = content_region {
            root_canvas.clip_rect(to_skia_rect(content_region), None, Some(false));
        } else if let Some(root_window) = self.rendered_windows.get(&1) {
            let clip_rect = to_skia_rect(&root_window.pixel_region(grid_scale));
            root_canvas.clip_rect(clip_rect, None, Some(false));
        }

        let (root_windows, floating_layers) = {
            let (mut root_windows, mut floating_windows): (
                Vec<&mut RenderedWindow>,
                Vec<&mut RenderedWindow>,
            ) = self
                .rendered_windows
                .values_mut()
                .filter(|window| !window.hidden)
                .partition(|window| window.anchor_info.is_none());

            root_windows
                .sort_by(|window_a, window_b| window_a.id.partial_cmp(&window_b.id).unwrap());
            floating_windows.sort_by(floating_sort);

            let mut floating_layers = vec![];

            let mut base_zindex = 0;
            let mut last_zindex = 0;
            let mut current_windows = vec![];

            let mut prev_is_message = false;
            for window in floating_windows {
                let zindex = window.anchor_info.as_ref().unwrap().sort_order.z_index;
                log::debug!("zindex: {zindex}, base: {base_zindex}");
                let is_message = matches!(window.window_type, WindowType::Message { .. });
                // NOTE: The message window is always on it's own layer
                if !current_windows.is_empty() && zindex != last_zindex
                    || is_message
                    || prev_is_message
                {
                    // Group floating windows by consecutive z indices if layer_grouping is enabled,
                    // Otherwise group all windows inside a single layer

                    if !layer_grouping || zindex - last_zindex > 1 {
                        for windows in group_windows(current_windows, grid_scale) {
                            floating_layers.push(FloatingLayer { windows });
                        }
                        current_windows = vec![];
                    }
                }
                prev_is_message = is_message;

                if current_windows.is_empty() {
                    base_zindex = zindex;
                }
                current_windows.push(window);
                last_zindex = zindex;
            }

            if !current_windows.is_empty() {
                for windows in group_windows(current_windows, grid_scale) {
                    floating_layers.push(FloatingLayer { windows });
                }
            }

            for layer in &mut floating_layers {
                layer.windows.sort_by(floating_sort);
                log::debug!(
                    "layer: {:?}",
                    layer
                        .windows
                        .iter()
                        .map(|w| (w.id, w.anchor_info.as_ref().unwrap().sort_order.clone()))
                        .collect_vec()
                );
            }

            (root_windows, floating_layers)
        };

        let settings = self.settings.get::<RendererSettings>();
        let max_root_x = max_window_max_x(&root_windows, grid_scale);
        let root_window_regions = root_windows
            .into_iter()
            .map(|window| {
                let region = window.pixel_region(grid_scale);
                let rightmost_root_window = is_rightmost_window_edge(region.max.x, max_root_x);
                window.draw(
                    root_canvas,
                    default_background,
                    grid_scale,
                    content_region.copied(),
                    rightmost_root_window,
                )
            })
            .collect_vec();

        let floating_window_regions = floating_layers
            .into_iter()
            .flat_map(|mut layer| {
                layer.draw(
                    root_canvas,
                    &settings,
                    default_background,
                    grid_scale,
                    content_region.copied(),
                )
            })
            .collect_vec();

        self.window_regions =
            root_window_regions.into_iter().chain(floating_window_regions).collect();
        self.draw_message_selection(root_canvas, grid_scale);
        self.cursor_renderer.draw(&mut self.grid_renderer, root_canvas);

        self.profiler.draw(root_canvas, dt);

        let grid_size = self.get_grid_size();

        root_canvas.restore();

        let progress_bar_settings = self.settings.get::<ProgressBarSettings>();
        self.progress_bar.draw(&progress_bar_settings, root_canvas, &self.grid_renderer, grid_size);

        #[cfg(feature = "profiling")]
        plot_skia_cache();
    }

    fn message_selection_window(&self) -> Option<(&MessageSelection, &RenderedWindow)> {
        let selection = self.message_selection.as_ref()?;
        let window = self.rendered_windows.get(&selection.grid_id)?;
        if !matches!(window.window_type, WindowType::Message { .. }) {
            return None;
        }

        Some((selection, window))
    }

    fn draw_message_selection(&mut self, root_canvas: &Canvas, grid_scale: GridScale) {
        let Some((selection, window)) = self.message_selection_window() else {
            return;
        };

        let height = window.grid_size.height;
        let width = window.grid_size.width;
        if height == 0 || width == 0 {
            return;
        }

        let max_row = height - 1;
        let max_col = width - 1;
        let start_row = selection.start.y.min(max_row);
        let end_row = selection.end.y.min(max_row);
        let (row_start, row_end) =
            if start_row <= end_row { (start_row, end_row) } else { (end_row, start_row) };

        let start_col = selection.start.x.min(max_col);
        let end_col = selection.end.x.min(max_col);
        let (col_start, col_end) =
            if start_col <= end_col { (start_col, end_col) } else { (end_col, start_col) };

        let pixel_region = window.pixel_region(grid_scale);
        root_canvas.save();
        root_canvas.clip_rect(to_skia_rect(&pixel_region), None, Some(false));

        let selection_color = self
            .grid_renderer
            .default_style
            .colors
            .foreground
            .or(self.grid_renderer.default_style.colors.background)
            .unwrap_or_else(|| Color4f::from(self.grid_renderer.get_default_background(1.0)));

        let mut paint = Paint::new(selection_color, None);
        paint.set_anti_alias(false);
        paint.set_alpha_f(MESSAGE_SELECTION_ALPHA);

        for row in row_start..=row_end {
            let row_start_col = if row == row_start { col_start } else { 0 };
            let row_end_col = if row == row_end { col_end } else { max_col };
            if let Some(rect) = window.grid_row_rect(row, row_start_col, row_end_col, grid_scale) {
                root_canvas.draw_rect(rect, &paint);
            }
        }

        root_canvas.restore();
    }

    pub fn animate_frame(&mut self, grid_rect: &GridRect<f32>, dt: f32) -> bool {
        let windows = {
            let (mut root_windows, mut floating_windows): (
                Vec<&mut RenderedWindow>,
                Vec<&mut RenderedWindow>,
            ) = self
                .rendered_windows
                .values_mut()
                .filter(|window| !window.hidden)
                .partition(|window| window.anchor_info.is_none());

            root_windows
                .sort_by(|window_a, window_b| window_a.id.partial_cmp(&window_b.id).unwrap());

            floating_windows.sort_by(floating_sort);

            root_windows.into_iter().chain(floating_windows)
        };

        let settings = self.settings.get::<RendererSettings>();
        // Clippy recommends short-circuiting with any which is not what we want
        #[allow(clippy::unnecessary_fold)]
        let mut animating =
            windows.fold(false, |acc, window| acc | window.animate(&settings, grid_rect, dt));

        let windows = &self.rendered_windows;
        let grid_scale = self.grid_renderer.grid_scale;
        self.cursor_renderer.update_cursor_destination(grid_scale, windows);

        animating |= self.cursor_renderer.animate(&self.current_mode, &self.grid_renderer, dt);

        let progress_bar_settings = self.settings.get::<ProgressBarSettings>();
        self.progress_bar.animate(&progress_bar_settings, dt);
        animating |= self.progress_bar.is_animating();

        animating
    }

    pub fn handle_config_changed(&mut self, config: RendererHotReloadConfigs) {
        match config {
            RendererHotReloadConfigs::Font(font) => {
                let font = *font;
                let mut font_config_state = self.settings.get::<FontConfigState>();
                font_config_state.has_font = font.is_some();
                self.settings.set(&font_config_state);
                match font {
                    Some(font) => {
                        self.grid_renderer.update_font_options(font.into());
                    }
                    None => {
                        self.grid_renderer.update_font_options(FontOptions::default());
                    }
                }
            }
            RendererHotReloadConfigs::BoxDrawing(settings) => {
                self.grid_renderer.handle_box_drawing_update(settings.unwrap_or_default())
            }
        }
    }

    pub fn handle_draw_commands(&mut self, batch: Vec<DrawCommand>) -> DrawCommandResult {
        let settings = self.settings.get::<RendererSettings>();
        let mut result = DrawCommandResult { font_changed: false, should_show: false };

        for draw_command in batch {
            self.handle_draw_command(draw_command, &mut result);
            tracy_named_frame!("neovim draw batch processed");
        }
        self.flush(&settings);

        result
    }

    pub fn sync_scale_factor(&mut self) {
        let user_scale_factor = self.settings.get::<WindowSettings>().scale_factor.into();
        self.handle_user_scale_factor_change(user_scale_factor);
    }

    pub fn handle_user_scale_factor_change(&mut self, user_scale_factor: f64) {
        self.user_scale_factor = user_scale_factor;
        self.update_scale_factor();
    }

    pub fn handle_os_scale_factor_change(&mut self, os_scale_factor: f64) {
        self.os_scale_factor = os_scale_factor;
        self.update_scale_factor();
    }

    fn update_scale_factor(&mut self) {
        self.grid_renderer
            .handle_scale_factor_update(self.os_scale_factor * self.user_scale_factor);
    }

    pub fn prepare_lines(&mut self, force: bool) {
        let opacity = self.settings.get::<WindowSettings>().opacity;
        self.rendered_windows
            .iter_mut()
            .for_each(|(_, w)| w.prepare_lines(&mut self.grid_renderer, opacity, force));
    }

    fn handle_draw_command(&mut self, draw_command: DrawCommand, result: &mut DrawCommandResult) {
        match draw_command {
            DrawCommand::Window { grid_id, command: WindowDrawCommand::Close } => {
                self.rendered_windows.remove(&grid_id);
            }
            DrawCommand::Window { grid_id, command } => {
                match self.rendered_windows.entry(grid_id) {
                    Entry::Occupied(mut occupied_entry) => {
                        let rendered_window = occupied_entry.get_mut();
                        rendered_window.handle_window_draw_command(command);
                    }
                    Entry::Vacant(vacant_entry) => match command {
                        WindowDrawCommand::Position { .. }
                        | WindowDrawCommand::ViewportMargins { .. } => {
                            let mut new_window = RenderedWindow::new(grid_id);
                            new_window.handle_window_draw_command(command);
                            vacant_entry.insert(new_window);
                        }
                        _ => {
                            let settings = self.settings.get::<CmdLineSettings>();
                            // Neovim can emit draw commands before a window is positioned when using multigrid.
                            if !settings.no_multi_grid {
                                log::warn!(
                                    "Ignoring {command:?} sent for uninitialized grid {grid_id}"
                                );
                            }
                        }
                    },
                }
            }
            DrawCommand::UpdateCursor(new_cursor) => {
                self.cursor_renderer.update_cursor(new_cursor);
            }
            DrawCommand::FontChanged(new_font) => {
                self.grid_renderer.update_font(&new_font);
                result.font_changed = true;
            }
            DrawCommand::LineSpaceChanged(new_linespace) => {
                self.grid_renderer.update_linespace(new_linespace);
                result.font_changed = true;
            }
            DrawCommand::DefaultStyleChanged(new_style) => {
                self.grid_renderer.default_style = Arc::new(new_style);
            }
            DrawCommand::ModeChanged(new_mode) => {
                self.current_mode = new_mode;
            }
            DrawCommand::UIReady => {
                result.should_show = true;
            }
        }
    }

    pub fn flush(&mut self, renderer_settings: &RendererSettings) {
        self.rendered_windows.iter_mut().for_each(|(_, w)| w.flush(renderer_settings));
    }

    pub fn clear(&mut self) {
        self.rendered_windows.clear();
        self.window_regions.clear();
        self.cursor_renderer = CursorRenderer::new(self.settings.clone());
        self.progress_bar = ProgressBar::new();
        self.current_mode = EditorMode::Unknown(String::new());
        self.message_selection = None;
    }

    pub fn get_cursor_destination(&self) -> PixelPos<f32> {
        self.cursor_renderer.get_destination()
    }

    pub fn get_grid_size(&self) -> GridSize<u32> {
        if let Some(main_grid) = self.rendered_windows.get(&1) {
            main_grid.grid_size
        } else {
            DEFAULT_GRID_SIZE
        }
    }
}

pub fn is_rightmost_window_edge(region_max_x: f32, max_x: f32) -> bool {
    max_x.is_finite() && (region_max_x - max_x).abs() <= f32::EPSILON
}

pub fn max_window_max_x(windows: &[&mut RenderedWindow], grid_scale: GridScale) -> f32 {
    windows.iter().fold(f32::NEG_INFINITY, |max_x, window| {
        let region = window.pixel_region(grid_scale);
        max_x.max(region.max.x)
    })
}

/// Defines how floating windows are sorted.
fn floating_sort(window_a: &&mut RenderedWindow, window_b: &&mut RenderedWindow) -> Ordering {
    let orda = &window_a.anchor_info.as_ref().unwrap().sort_order;
    let ordb = &window_b.anchor_info.as_ref().unwrap().sort_order;
    orda.cmp(ordb)
}

#[derive(Clone)]
pub enum WindowConfigType {
    OpenGL(glutin::config::Config),
    #[cfg(target_os = "windows")]
    Direct3D,
    #[cfg(target_os = "macos")]
    Metal,
}

#[derive(Clone)]
pub struct WindowConfig {
    pub window: Rc<Window>,
    pub config: WindowConfigType,
}

#[cfg(target_os = "macos")]
pub fn build_window_config(
    window_attributes: WindowAttributes,
    event_loop: &ActiveEventLoop,
    settings: &Settings,
) -> WindowConfig {
    let cmd_line_settings = settings.get::<CmdLineSettings>();
    if cmd_line_settings.opengl {
        opengl::build_window(window_attributes, event_loop)
    } else {
        let window = event_loop.create_window(window_attributes).unwrap();
        let config = WindowConfigType::Metal;
        WindowConfig { window: window.into(), config }
    }
}

#[cfg(target_os = "windows")]
pub fn build_window_config(
    window_attributes: WindowAttributes,
    event_loop: &ActiveEventLoop,
    settings: &Settings,
) -> WindowConfig {
    let cmd_line_settings = settings.get::<CmdLineSettings>();
    if cmd_line_settings.opengl {
        opengl::build_window(window_attributes, event_loop)
    } else {
        let window = event_loop.create_window(window_attributes).unwrap();
        let config = WindowConfigType::Direct3D;
        WindowConfig { window: window.into(), config }
    }
}

#[cfg(not(any(target_os = "windows", target_os = "macos")))]
pub fn build_window_config(
    window_attributes: WindowAttributes,
    event_loop: &ActiveEventLoop,
    _settings: &Settings,
) -> WindowConfig {
    opengl::build_window(window_attributes, event_loop)
}

pub trait SkiaRenderer {
    fn window(&self) -> Rc<Window>;
    fn flush(&mut self);
    fn swap_buffers(&mut self);
    fn canvas(&mut self) -> &Canvas;
    fn resize(&mut self);
    fn create_vsync(&self, proxy: EventLoopProxy<EventPayload>) -> VSync;
    #[cfg(feature = "gpu_profiling")]
    fn tracy_create_gpu_context(&self, name: &str) -> Box<dyn GpuCtx>;
}

pub fn create_skia_renderer(
    window: &WindowConfig,
    srgb: bool,
    vsync: bool,
    settings: Arc<Settings>,
) -> Box<dyn SkiaRenderer> {
    let renderer: Box<dyn SkiaRenderer> = match &window.config {
        WindowConfigType::OpenGL(..) => {
            Box::new(opengl::OpenGLSkiaRenderer::new(window.clone(), srgb, vsync, settings.clone()))
        }
        #[cfg(target_os = "windows")]
        WindowConfigType::Direct3D => {
            Box::new(d3d::D3DSkiaRenderer::new(window.window.clone(), settings.clone()))
        }
        #[cfg(target_os = "macos")]
        WindowConfigType::Metal => Box::new(metal::MetalSkiaRenderer::new(
            window.window.clone(),
            srgb,
            vsync,
            settings.clone(),
        )),
    };
    tracy_create_gpu_context("main_render_context", renderer.as_ref());
    renderer
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::*;

    fn create_renderer() -> Renderer {
        let settings = Arc::new(Settings::new());
        settings.register::<WindowSettings>();
        Renderer::new(2.0, Config::default(), settings)
    }

    #[test]
    fn user_scale_updates_apply_immediately() {
        let mut renderer = create_renderer();
        let initial_size = renderer.grid_renderer.shaper.current_size();

        renderer.handle_user_scale_factor_change(1.5);

        assert_eq!(renderer.user_scale_factor, 1.5);
        assert_eq!(renderer.grid_renderer.shaper.current_size(), initial_size * 1.5);
    }

    #[test]
    fn scale_can_be_resynced_from_settings_after_renderer_creation() {
        let settings = Arc::new(Settings::new());
        settings.register::<WindowSettings>();

        let mut renderer = Renderer::new(1.0, Config::default(), settings.clone());
        let initial_size = renderer.grid_renderer.shaper.current_size();

        let mut window_settings = settings.get::<WindowSettings>();
        window_settings.scale_factor = 1.5;
        settings.set(&window_settings);

        renderer.sync_scale_factor();

        assert_eq!(renderer.user_scale_factor, 1.5);
        assert_eq!(renderer.grid_renderer.shaper.current_size(), initial_size * 1.5);
    }
}
