mod binding;
pub(super) mod drawer;
// Consts and bind groups for post-process and clouds
mod locals;
mod pipeline_creation;
mod screenshot;
mod shaders;
mod shadow_map;

use locals::Locals;
use pipeline_creation::{
    IngameAndShadowPipelines, InterfacePipelines, PipelineCreation, Pipelines, ShadowPipelines,
};
use shaders::Shaders;
use shadow_map::{ShadowMap, ShadowMapRenderer};

use super::{
    buffer::Buffer,
    consts::Consts,
    instances::Instances,
    mesh::Mesh,
    model::{DynamicModel, Model},
    pipelines::{
        blit, bloom, clouds, debug, figure, lod_object, postprocess, shadow, sprite, terrain, ui,
        GlobalsBindGroup, GlobalsLayouts, ShadowTexturesBindGroup,
    },
    texture::Texture,
    AaMode, AddressMode, FilterMode, OtherModes, PipelineModes, RenderError, RenderMode,
    ShadowMapMode, ShadowMode, Vertex,
};
use common::assets::{self, AssetExt, AssetHandle, ReloadWatcher};
use common_base::span;
use core::convert::TryFrom;
#[cfg(feature = "egui-ui")]
use egui_wgpu_backend::wgpu::TextureFormat;
use std::sync::Arc;
use tracing::{error, info, warn};
use vek::*;

// TODO: yeet this somewhere else
/// A type representing data that can be converted to an immutable texture map
/// of ColLight data (used for texture atlases created during greedy meshing).
// TODO: revert to u16
pub type ColLightInfo = (Vec<[u8; 4]>, Vec2<u16>);

const QUAD_INDEX_BUFFER_U16_START_VERT_LEN: u16 = 3000;
const QUAD_INDEX_BUFFER_U32_START_VERT_LEN: u32 = 3000;

/// A type that stores all the layouts associated with this renderer that never
/// change when the RenderMode is modified.
struct ImmutableLayouts {
    global: GlobalsLayouts,

    debug: debug::DebugLayout,
    figure: figure::FigureLayout,
    shadow: shadow::ShadowLayout,
    sprite: sprite::SpriteLayout,
    terrain: terrain::TerrainLayout,
    clouds: clouds::CloudsLayout,
    bloom: bloom::BloomLayout,
    ui: ui::UiLayout,
    blit: blit::BlitLayout,
}

/// A type that stores all the layouts associated with this renderer.
struct Layouts {
    immutable: Arc<ImmutableLayouts>,

    postprocess: Arc<postprocess::PostProcessLayout>,
}

impl core::ops::Deref for Layouts {
    type Target = ImmutableLayouts;

    fn deref(&self) -> &Self::Target { &self.immutable }
}

/// Render target views
struct Views {
    // NOTE: unused for now, maybe... we will want it for something
    _win_depth: wgpu::TextureView,

    tgt_color: wgpu::TextureView,
    tgt_depth: wgpu::TextureView,

    bloom_tgts: Option<[wgpu::TextureView; bloom::NUM_SIZES]>,
    // TODO: rename
    tgt_color_pp: wgpu::TextureView,
}

/// Shadow rendering textures, layouts, pipelines, and bind groups
struct Shadow {
    map: ShadowMap,
    bind: ShadowTexturesBindGroup,
}

/// Represent two states of the renderer:
/// 1. Only interface pipelines created
/// 2. All of the pipelines have been created
#[allow(clippy::large_enum_variant)] // They are both pretty large
enum State {
    // NOTE: this is used as a transient placeholder for moving things out of State temporarily
    Nothing,
    Interface {
        pipelines: InterfacePipelines,
        shadow_views: Option<(Texture, Texture)>,
        // In progress creation of the remaining pipelines in the background
        creating: PipelineCreation<IngameAndShadowPipelines>,
    },
    Complete {
        pipelines: Pipelines,
        shadow: Shadow,
        recreating: Option<(
            PipelineModes,
            PipelineCreation<
                Result<
                    (
                        Pipelines,
                        ShadowPipelines,
                        Arc<postprocess::PostProcessLayout>,
                    ),
                    RenderError,
                >,
            >,
        )>,
    },
}

/// A type that encapsulates rendering state. `Renderer` is central to Voxygen's
/// rendering subsystem and contains any state necessary to interact with the
/// GPU, along with pipeline state objects (PSOs) needed to renderer different
/// kinds of models to the screen.
pub struct Renderer {
    device: Arc<wgpu::Device>,
    queue: wgpu::Queue,
    surface: wgpu::Surface,
    swap_chain: wgpu::SwapChain,
    sc_desc: wgpu::SwapChainDescriptor,

    sampler: wgpu::Sampler,
    depth_sampler: wgpu::Sampler,

    state: State,
    // Some if there is a pending need to recreate the pipelines (e.g. RenderMode change or shader
    // hotloading)
    recreation_pending: Option<PipelineModes>,

    layouts: Layouts,
    // Note: we keep these here since their bind groups need to be updated if we resize the
    // color/depth textures
    locals: Locals,
    views: Views,
    noise_tex: Texture,

    quad_index_buffer_u16: Buffer<u16>,
    quad_index_buffer_u32: Buffer<u32>,

    shaders: AssetHandle<Shaders>,
    shaders_watcher: ReloadWatcher,

    pipeline_modes: PipelineModes,
    other_modes: OtherModes,
    resolution: Vec2<u32>,

    // If this is Some then a screenshot will be taken and passed to the handler here
    take_screenshot: Option<screenshot::ScreenshotFn>,

    profiler: wgpu_profiler::GpuProfiler,
    profile_times: Vec<wgpu_profiler::GpuTimerScopeResult>,
    profiler_features_enabled: bool,

    #[cfg(feature = "egui-ui")]
    egui_renderpass: egui_wgpu_backend::RenderPass,

    // This checks is added because windows resizes the window to 0,0 when
    // minimizing and this causes a bunch of validation errors
    is_minimized: bool,

    // To remember the backend info after initialization for debug purposes
    graphics_backend: String,
}

impl Renderer {
    /// Create a new `Renderer` from a variety of backend-specific components
    /// and the window targets.
    pub fn new(
        window: &winit::window::Window,
        mode: RenderMode,
        runtime: &tokio::runtime::Runtime,
    ) -> Result<Self, RenderError> {
        let (pipeline_modes, mut other_modes) = mode.split();
        // Enable seamless cubemaps globally, where available--they are essentially a
        // strict improvement on regular cube maps.
        //
        // Note that since we only have to enable this once globally, there is no point
        // in doing this on rerender.
        // Self::enable_seamless_cube_maps(&mut device);

        // TODO: fix panic on wayland with opengl?
        // TODO: fix backend defaulting to opengl on wayland.
        let backend_bit = std::env::var("WGPU_BACKEND")
            .ok()
            .and_then(|backend| match backend.to_lowercase().as_str() {
                "vulkan" => Some(wgpu::BackendBit::VULKAN),
                "metal" => Some(wgpu::BackendBit::METAL),
                "dx12" => Some(wgpu::BackendBit::DX12),
                "primary" => Some(wgpu::BackendBit::PRIMARY),
                "opengl" | "gl" => Some(wgpu::BackendBit::GL),
                "dx11" => Some(wgpu::BackendBit::DX11),
                "secondary" => Some(wgpu::BackendBit::SECONDARY),
                "all" => Some(wgpu::BackendBit::all()),
                _ => None,
            })
            .unwrap_or(
                (wgpu::BackendBit::PRIMARY | wgpu::BackendBit::SECONDARY) & !wgpu::BackendBit::GL,
            );

        let instance = wgpu::Instance::new(backend_bit);

        let dims = window.inner_size();

        // This is unsafe because the window handle must be valid, if you find a way to
        // have an invalid winit::Window then you have bigger issues
        #[allow(unsafe_code)]
        let surface = unsafe { instance.create_surface(window) };

        let adapters = instance
            .enumerate_adapters(backend_bit)
            .enumerate()
            .collect::<Vec<_>>();

        for (i, adapter) in adapters.iter() {
            let info = adapter.get_info();
            info!(
                ?info.name,
                ?info.vendor,
                ?info.backend,
                ?info.device,
                ?info.device_type,
                "graphics device #{}", i,
            );
        }

        let adapter = match std::env::var("WGPU_ADAPTER").ok() {
            Some(filter) if !filter.is_empty() => adapters.into_iter().find_map(|(i, adapter)| {
                let info = adapter.get_info();

                let full_name = format!("#{} {} {:?}", i, info.name, info.device_type,);

                full_name.contains(&filter).then(|| adapter)
            }),
            Some(_) | None => {
                runtime.block_on(instance.request_adapter(&wgpu::RequestAdapterOptionsBase {
                    power_preference: wgpu::PowerPreference::HighPerformance,
                    compatible_surface: Some(&surface),
                }))
            },
        }
        .ok_or(RenderError::CouldNotFindAdapter)?;

        let info = adapter.get_info();
        info!(
            ?info.name,
            ?info.vendor,
            ?info.backend,
            ?info.device,
            ?info.device_type,
            "selected graphics device"
        );
        let graphics_backend = format!("{:?}", &info.backend);

        let limits = wgpu::Limits {
            max_push_constant_size: 64,
            ..Default::default()
        };

        let trace_env = std::env::var_os("WGPU_TRACE_DIR");
        let trace_path = trace_env.as_ref().map(|v| {
            let path = std::path::Path::new(v);
            // We don't want to continue if we can't actually collect the api trace
            assert!(
                path.exists(),
                "WGPU_TRACE_DIR is set to the path \"{}\" which doesn't exist",
                path.display()
            );
            assert!(
                path.is_dir(),
                "WGPU_TRACE_DIR is set to the path \"{}\" which is not a directory",
                path.display()
            );
            assert!(
                path.read_dir()
                    .expect("Could not read the directory that is specified by WGPU_TRACE_DIR")
                    .next()
                    .is_none(),
                "WGPU_TRACE_DIR is set to the path \"{}\" which already contains other files",
                path.display()
            );

            path
        });

        let (device, queue) = runtime.block_on(adapter.request_device(
            &wgpu::DeviceDescriptor {
                // TODO
                label: None,
                features: wgpu::Features::DEPTH_CLAMPING
                    | wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER
                    | wgpu::Features::PUSH_CONSTANTS
                    | (adapter.features() & wgpu_profiler::GpuProfiler::REQUIRED_WGPU_FEATURES),
                limits,
            },
            trace_path,
        ))?;

        // Set error handler for wgpu errors
        // This is better for use than their default because it includes the error in
        // the panic message
        device.on_uncaptured_error(move |error| {
            error!("{}", &error);
            panic!(
                "wgpu error (handling all wgpu errors as fatal):\n{:?}\n{:?}",
                &error, &info,
            );
        });

        let profiler_features_enabled = device
            .features()
            .contains(wgpu_profiler::GpuProfiler::REQUIRED_WGPU_FEATURES);
        if !profiler_features_enabled {
            info!(
                "The features for GPU profiling (timestamp queries) are not available on this \
                 adapter"
            );
        }

        let format = adapter
            .get_swap_chain_preferred_format(&surface)
            .expect("No supported swap chain format found");
        info!("Using {:?} as the swapchain format", format);

        let sc_desc = wgpu::SwapChainDescriptor {
            usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
            format,
            width: dims.width,
            height: dims.height,
            present_mode: other_modes.present_mode.into(),
        };

        let swap_chain = device.create_swap_chain(&surface, &sc_desc);

        let shadow_views = ShadowMap::create_shadow_views(
            &device,
            (dims.width, dims.height),
            &ShadowMapMode::try_from(pipeline_modes.shadow).unwrap_or_default(),
        )
        .map_err(|err| {
            warn!("Could not create shadow map views: {:?}", err);
        })
        .ok();

        let shaders = Shaders::load_expect("");
        let shaders_watcher = shaders.reload_watcher();

        let layouts = {
            let global = GlobalsLayouts::new(&device);

            let debug = debug::DebugLayout::new(&device);
            let figure = figure::FigureLayout::new(&device);
            let shadow = shadow::ShadowLayout::new(&device);
            let sprite = sprite::SpriteLayout::new(&device);
            let terrain = terrain::TerrainLayout::new(&device);
            let clouds = clouds::CloudsLayout::new(&device);
            let bloom = bloom::BloomLayout::new(&device);
            let postprocess = Arc::new(postprocess::PostProcessLayout::new(
                &device,
                &pipeline_modes,
            ));
            let ui = ui::UiLayout::new(&device);
            let blit = blit::BlitLayout::new(&device);

            let immutable = Arc::new(ImmutableLayouts {
                global,

                debug,
                figure,
                shadow,
                sprite,
                terrain,
                clouds,
                bloom,
                ui,
                blit,
            });

            Layouts {
                immutable,
                postprocess,
            }
        };

        // Arcify the device
        let device = Arc::new(device);

        let (interface_pipelines, creating) = pipeline_creation::initial_create_pipelines(
            Arc::clone(&device),
            Layouts {
                immutable: Arc::clone(&layouts.immutable),
                postprocess: Arc::clone(&layouts.postprocess),
            },
            shaders.cloned(),
            pipeline_modes.clone(),
            sc_desc.clone(), // Note: cheap clone
            shadow_views.is_some(),
        )?;

        let state = State::Interface {
            pipelines: interface_pipelines,
            shadow_views,
            creating,
        };

        let (views, bloom_sizes) = Self::create_rt_views(
            &device,
            (dims.width, dims.height),
            &pipeline_modes,
            &other_modes,
        );

        let create_sampler = |filter| {
            device.create_sampler(&wgpu::SamplerDescriptor {
                label: None,
                address_mode_u: wgpu::AddressMode::ClampToEdge,
                address_mode_v: wgpu::AddressMode::ClampToEdge,
                address_mode_w: wgpu::AddressMode::ClampToEdge,
                mag_filter: filter,
                min_filter: filter,
                mipmap_filter: wgpu::FilterMode::Nearest,
                compare: None,
                ..Default::default()
            })
        };

        let sampler = create_sampler(wgpu::FilterMode::Linear);
        let depth_sampler = create_sampler(wgpu::FilterMode::Nearest);

        let noise_tex = Texture::new(
            &device,
            &queue,
            &assets::Image::load_expect("voxygen.texture.noise").read().0,
            Some(wgpu::FilterMode::Linear),
            Some(wgpu::AddressMode::Repeat),
        )?;

        let clouds_locals =
            Self::create_consts_inner(&device, &queue, &[clouds::Locals::default()]);
        let postprocess_locals =
            Self::create_consts_inner(&device, &queue, &[postprocess::Locals::default()]);

        let locals = Locals::new(
            &device,
            &layouts,
            clouds_locals,
            postprocess_locals,
            &views.tgt_color,
            &views.tgt_depth,
            views.bloom_tgts.as_ref().map(|tgts| locals::BloomParams {
                locals: bloom_sizes.map(|size| {
                    Self::create_consts_inner(&device, &queue, &[bloom::Locals::new(size)])
                }),
                src_views: [&views.tgt_color_pp, &tgts[1], &tgts[2], &tgts[3], &tgts[4]],
                final_tgt_view: &tgts[0],
            }),
            &views.tgt_color_pp,
            &sampler,
            &depth_sampler,
        );

        let quad_index_buffer_u16 =
            create_quad_index_buffer_u16(&device, QUAD_INDEX_BUFFER_U16_START_VERT_LEN as usize);
        let quad_index_buffer_u32 =
            create_quad_index_buffer_u32(&device, QUAD_INDEX_BUFFER_U32_START_VERT_LEN as usize);
        let mut profiler = wgpu_profiler::GpuProfiler::new(4, queue.get_timestamp_period());
        other_modes.profiler_enabled &= profiler_features_enabled;
        profiler.enable_timer = other_modes.profiler_enabled;
        profiler.enable_debug_marker = other_modes.profiler_enabled;

        #[cfg(feature = "egui-ui")]
        let egui_renderpass =
            egui_wgpu_backend::RenderPass::new(&*device, TextureFormat::Bgra8UnormSrgb, 1);

        Ok(Self {
            device,
            queue,
            surface,
            swap_chain,
            sc_desc,

            state,
            recreation_pending: None,

            layouts,
            locals,
            views,

            sampler,
            depth_sampler,
            noise_tex,

            quad_index_buffer_u16,
            quad_index_buffer_u32,

            shaders,
            shaders_watcher,

            pipeline_modes,
            other_modes,
            resolution: Vec2::new(dims.width, dims.height),

            take_screenshot: None,

            profiler,
            profile_times: Vec::new(),
            profiler_features_enabled,

            #[cfg(feature = "egui-ui")]
            egui_renderpass,

            is_minimized: false,

            graphics_backend,
        })
    }

    /// Get the graphics backend being used
    pub fn graphics_backend(&self) -> &str { &self.graphics_backend }

    /// Check the status of the intial pipeline creation
    /// Returns `None` if complete
    /// Returns `Some((total, complete))` if in progress
    pub fn pipeline_creation_status(&self) -> Option<(usize, usize)> {
        if let State::Interface { creating, .. } = &self.state {
            Some(creating.status())
        } else {
            None
        }
    }

    /// Check the status the pipeline recreation
    /// Returns `None` if pipelines are currently not being recreated
    /// Returns `Some((total, complete))` if in progress
    pub fn pipeline_recreation_status(&self) -> Option<(usize, usize)> {
        if let State::Complete { recreating, .. } = &self.state {
            recreating.as_ref().map(|(_, c)| c.status())
        } else {
            None
        }
    }

    /// Change the render mode.
    pub fn set_render_mode(&mut self, mode: RenderMode) -> Result<(), RenderError> {
        let (pipeline_modes, other_modes) = mode.split();

        if self.other_modes != other_modes {
            self.other_modes = other_modes;

            // Update present mode in swap chain descriptor
            self.sc_desc.present_mode = self.other_modes.present_mode.into();

            // Only enable profiling if the wgpu features are enabled
            self.other_modes.profiler_enabled &= self.profiler_features_enabled;
            // Enable/disable profiler
            if !self.other_modes.profiler_enabled {
                // Clear the times if disabled
                core::mem::take(&mut self.profile_times);
            }
            self.profiler.enable_timer = self.other_modes.profiler_enabled;
            self.profiler.enable_debug_marker = self.other_modes.profiler_enabled;

            // Recreate render target
            self.on_resize(self.resolution);
        }

        // We can't cancel the pending recreation even if the new settings are equal
        // to the current ones becuase the recreation could be triggered by something
        // else like shader hotloading
        if self.pipeline_modes != pipeline_modes
            || self
                .recreation_pending
                .as_ref()
                .map_or(false, |modes| modes != &pipeline_modes)
        {
            // Recreate pipelines with new modes
            self.recreate_pipelines(pipeline_modes);
        }

        Ok(())
    }

    /// Get the pipelines mode.
    pub fn pipeline_modes(&self) -> &PipelineModes { &self.pipeline_modes }

    /// Get the current profiling times
    /// Nested timings immediately follow their parent
    /// Returns Vec<(how nested this timing is, label, length in seconds)>
    pub fn timings(&self) -> Vec<(u8, &str, f64)> {
        use wgpu_profiler::GpuTimerScopeResult;
        fn recursive_collect<'a>(
            vec: &mut Vec<(u8, &'a str, f64)>,
            result: &'a GpuTimerScopeResult,
            nest_level: u8,
        ) {
            vec.push((
                nest_level,
                &result.label,
                result.time.end - result.time.start,
            ));
            result
                .nested_scopes
                .iter()
                .for_each(|child| recursive_collect(vec, child, nest_level + 1));
        }
        let mut vec = Vec::new();
        self.profile_times
            .iter()
            .for_each(|child| recursive_collect(&mut vec, child, 0));
        vec
    }

    /// Resize internal render targets to match window render target dimensions.
    pub fn on_resize(&mut self, dims: Vec2<u32>) {
        // Avoid panics when creating texture with w,h of 0,0.
        if dims.x != 0 && dims.y != 0 {
            self.is_minimized = false;
            // Resize swap chain
            self.resolution = dims;
            self.sc_desc.width = dims.x;
            self.sc_desc.height = dims.y;
            self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc);

            // Resize other render targets
            let (views, bloom_sizes) = Self::create_rt_views(
                &self.device,
                (dims.x, dims.y),
                &self.pipeline_modes,
                &self.other_modes,
            );
            self.views = views;

            let bloom_params = self
                .views
                .bloom_tgts
                .as_ref()
                .map(|tgts| locals::BloomParams {
                    locals: bloom_sizes.map(|size| {
                        Self::create_consts_inner(&self.device, &self.queue, &[bloom::Locals::new(
                            size,
                        )])
                    }),
                    src_views: [
                        &self.views.tgt_color_pp,
                        &tgts[1],
                        &tgts[2],
                        &tgts[3],
                        &tgts[4],
                    ],
                    final_tgt_view: &tgts[0],
                });

            self.locals.rebind(
                &self.device,
                &self.layouts,
                &self.views.tgt_color,
                &self.views.tgt_depth,
                bloom_params,
                &self.views.tgt_color_pp,
                &self.sampler,
                &self.depth_sampler,
            );

            // Get mutable reference to shadow views out of the current state
            let shadow_views = match &mut self.state {
                State::Interface { shadow_views, .. } => {
                    shadow_views.as_mut().map(|s| (&mut s.0, &mut s.1))
                },
                State::Complete {
                    shadow:
                        Shadow {
                            map: ShadowMap::Enabled(shadow_map),
                            ..
                        },
                    ..
                } => Some((&mut shadow_map.point_depth, &mut shadow_map.directed_depth)),
                State::Complete { .. } => None,
                State::Nothing => None, // Should never hit this
            };

            if let (Some((point_depth, directed_depth)), ShadowMode::Map(mode)) =
                (shadow_views, self.pipeline_modes.shadow)
            {
                match ShadowMap::create_shadow_views(&self.device, (dims.x, dims.y), &mode) {
                    Ok((new_point_depth, new_directed_depth)) => {
                        *point_depth = new_point_depth;
                        *directed_depth = new_directed_depth;
                        // Recreate the shadow bind group if needed
                        if let State::Complete {
                            shadow:
                                Shadow {
                                    bind,
                                    map: ShadowMap::Enabled(shadow_map),
                                    ..
                                },
                            ..
                        } = &mut self.state
                        {
                            *bind = self.layouts.global.bind_shadow_textures(
                                &self.device,
                                &shadow_map.point_depth,
                                &shadow_map.directed_depth,
                            );
                        }
                    },
                    Err(err) => {
                        warn!("Could not create shadow map views: {:?}", err);
                    },
                }
            }
        } else {
            self.is_minimized = true;
        }
    }

    pub fn maintain(&self) {
        if self.is_minimized {
            self.queue.submit(std::iter::empty());
        }

        self.device.poll(wgpu::Maintain::Poll)
    }

    /// Create render target views
    fn create_rt_views(
        device: &wgpu::Device,
        size: (u32, u32),
        pipeline_modes: &PipelineModes,
        other_modes: &OtherModes,
    ) -> (Views, [Vec2<f32>; bloom::NUM_SIZES]) {
        let upscaled = Vec2::<u32>::from(size)
            .map(|e| (e as f32 * other_modes.upscale_mode.factor) as u32)
            .into_tuple();
        let (width, height, sample_count) = match pipeline_modes.aa {
            AaMode::None | AaMode::Fxaa => (upscaled.0, upscaled.1, 1),
            AaMode::MsaaX4 => (upscaled.0, upscaled.1, 4),
            AaMode::MsaaX8 => (upscaled.0, upscaled.1, 8),
            AaMode::MsaaX16 => (upscaled.0, upscaled.1, 16),
        };
        let levels = 1;

        let color_view = |width, height| {
            let tex = device.create_texture(&wgpu::TextureDescriptor {
                label: None,
                size: wgpu::Extent3d {
                    width,
                    height,
                    depth_or_array_layers: 1,
                },
                mip_level_count: levels,
                sample_count,
                dimension: wgpu::TextureDimension::D2,
                format: wgpu::TextureFormat::Rgba16Float,
                usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::RENDER_ATTACHMENT,
            });

            tex.create_view(&wgpu::TextureViewDescriptor {
                label: None,
                format: Some(wgpu::TextureFormat::Rgba16Float),
                dimension: Some(wgpu::TextureViewDimension::D2),
                // TODO: why is this not Color?
                aspect: wgpu::TextureAspect::All,
                base_mip_level: 0,
                mip_level_count: None,
                base_array_layer: 0,
                array_layer_count: None,
            })
        };

        let tgt_color_view = color_view(width, height);
        let tgt_color_pp_view = color_view(width, height);

        let mut size_shift = 0;
        // TODO: skip creating bloom stuff when it is disabled
        let bloom_sizes = [(); bloom::NUM_SIZES].map(|()| {
            // .max(1) to ensure we don't create zero sized textures
            let size = Vec2::new(width, height).map(|e| (e >> size_shift).max(1));
            size_shift += 1;
            size
        });

        let bloom_tgt_views = pipeline_modes
            .bloom
            .is_on()
            .then(|| bloom_sizes.map(|size| color_view(size.x, size.y)));

        let tgt_depth_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: None,
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: levels,
            sample_count,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Depth32Float,
            usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::RENDER_ATTACHMENT,
        });
        let tgt_depth_view = tgt_depth_tex.create_view(&wgpu::TextureViewDescriptor {
            label: None,
            format: Some(wgpu::TextureFormat::Depth32Float),
            dimension: Some(wgpu::TextureViewDimension::D2),
            aspect: wgpu::TextureAspect::DepthOnly,
            base_mip_level: 0,
            mip_level_count: None,
            base_array_layer: 0,
            array_layer_count: None,
        });

        let win_depth_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: None,
            size: wgpu::Extent3d {
                width: size.0,
                height: size.1,
                depth_or_array_layers: 1,
            },
            mip_level_count: levels,
            sample_count,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Depth32Float,
            usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
        });
        // TODO: Consider no depth buffer for the final draw to the window?
        let win_depth_view = win_depth_tex.create_view(&wgpu::TextureViewDescriptor {
            label: None,
            format: Some(wgpu::TextureFormat::Depth32Float),
            dimension: Some(wgpu::TextureViewDimension::D2),
            aspect: wgpu::TextureAspect::DepthOnly,
            base_mip_level: 0,
            mip_level_count: None,
            base_array_layer: 0,
            array_layer_count: None,
        });

        (
            Views {
                tgt_color: tgt_color_view,
                tgt_depth: tgt_depth_view,
                bloom_tgts: bloom_tgt_views,
                tgt_color_pp: tgt_color_pp_view,
                _win_depth: win_depth_view,
            },
            bloom_sizes.map(|s| s.map(|e| e as f32)),
        )
    }

    /// Get the resolution of the render target.
    pub fn resolution(&self) -> Vec2<u32> { self.resolution }

    /// Get the resolution of the shadow render target.
    pub fn get_shadow_resolution(&self) -> (Vec2<u32>, Vec2<u32>) {
        match &self.state {
            State::Interface { shadow_views, .. } => shadow_views.as_ref().map(|s| (&s.0, &s.1)),
            State::Complete {
                shadow:
                    Shadow {
                        map: ShadowMap::Enabled(shadow_map),
                        ..
                    },
                ..
            } => Some((&shadow_map.point_depth, &shadow_map.directed_depth)),
            State::Complete { .. } | State::Nothing => None,
        }
        .map(|(point, directed)| (point.get_dimensions().xy(), directed.get_dimensions().xy()))
        .unwrap_or_else(|| (Vec2::new(1, 1), Vec2::new(1, 1)))
    }

    // TODO: Seamless is potentially the default with wgpu but we need further
    // investigation into whether this is actually turned on for the OpenGL
    // backend
    //
    /// NOTE: Supported by Vulkan (by default), DirectX 10+ (it seems--it's hard
    /// to find proof of this, but Direct3D 10 apparently does it by
    /// default, and 11 definitely does, so I assume it's natively supported
    /// by DirectX itself), OpenGL 3.2+, and Metal (done by default).  While
    /// there may be some GPUs that don't quite support it correctly, the
    /// impact is relatively small, so there is no reason not to enable it where
    /// available.
    //fn enable_seamless_cube_maps() {
    //todo!()
    // unsafe {
    //     // NOTE: Currently just fail silently rather than complain if the
    // computer is on     // a version lower than 3.2, where
    // seamless cubemaps were introduced.     if !device.get_info().
    // is_version_supported(3, 2) {         return;
    //     }

    //     // NOTE: Safe because GL_TEXTURE_CUBE_MAP_SEAMLESS is supported
    // by OpenGL 3.2+     // (see https://www.khronos.org/opengl/wiki/Cubemap_Texture#Seamless_cubemap);
    //     // enabling seamless cube maps should always be safe regardless
    // of the state of     // the OpenGL context, so no further
    // checks are needed.     device.with_gl(|gl| {
    //         gl.Enable(gfx_gl::TEXTURE_CUBE_MAP_SEAMLESS);
    //     });
    // }
    //}

    /// Start recording the frame
    /// When the returned `Drawer` is dropped the recorded draw calls will be
    /// submitted to the queue
    /// If there is an intermittent issue with the swap chain then Ok(None) will
    /// be returned
    pub fn start_recording_frame<'a>(
        &'a mut self,
        globals: &'a GlobalsBindGroup,
    ) -> Result<Option<drawer::Drawer<'a>>, RenderError> {
        span!(
            _guard,
            "start_recording_frame",
            "Renderer::start_recording_frame"
        );

        if self.is_minimized {
            return Ok(None);
        }

        // Try to get the latest profiling results
        if self.other_modes.profiler_enabled {
            // Note: this lags a few frames behind
            if let Some(profile_times) = self.profiler.process_finished_frame() {
                self.profile_times = profile_times;
            }
        }

        // Handle polling background pipeline creation/recreation
        // Temporarily set to nothing and then replace in the statement below
        let state = core::mem::replace(&mut self.state, State::Nothing);
        // Indicator for if pipeline recreation finished and we need to recreate bind
        // groups / render targets (handling defered so that State will be valid
        // when calling Self::on_resize)
        let mut trigger_on_resize = false;
        // If still creating initial pipelines, check if complete
        self.state = if let State::Interface {
            pipelines: interface,
            shadow_views,
            creating,
        } = state
        {
            match creating.try_complete() {
                Ok(pipelines) => {
                    let IngameAndShadowPipelines { ingame, shadow } = pipelines;

                    let pipelines = Pipelines::consolidate(interface, ingame);

                    let shadow_map = ShadowMap::new(
                        &self.device,
                        &self.queue,
                        shadow.point,
                        shadow.directed,
                        shadow.figure,
                        shadow_views,
                    );

                    let shadow_bind = {
                        let (point, directed) = shadow_map.textures();
                        self.layouts
                            .global
                            .bind_shadow_textures(&self.device, point, directed)
                    };

                    let shadow = Shadow {
                        map: shadow_map,
                        bind: shadow_bind,
                    };

                    State::Complete {
                        pipelines,
                        shadow,
                        recreating: None,
                    }
                },
                // Not complete
                Err(creating) => State::Interface {
                    pipelines: interface,
                    shadow_views,
                    creating,
                },
            }
        // If recreating the pipelines, check if that is complete
        } else if let State::Complete {
            pipelines,
            mut shadow,
            recreating: Some((new_pipeline_modes, pipeline_creation)),
        } = state
        {
            match pipeline_creation.try_complete() {
                Ok(Ok((pipelines, shadow_pipelines, postprocess_layout))) => {
                    if let (
                        Some(point_pipeline),
                        Some(terrain_directed_pipeline),
                        Some(figure_directed_pipeline),
                        ShadowMap::Enabled(shadow_map),
                    ) = (
                        shadow_pipelines.point,
                        shadow_pipelines.directed,
                        shadow_pipelines.figure,
                        &mut shadow.map,
                    ) {
                        shadow_map.point_pipeline = point_pipeline;
                        shadow_map.terrain_directed_pipeline = terrain_directed_pipeline;
                        shadow_map.figure_directed_pipeline = figure_directed_pipeline;
                    }

                    self.pipeline_modes = new_pipeline_modes;
                    self.layouts.postprocess = postprocess_layout;
                    // TODO: we have the potential to skip recreating bindings / render targets on
                    // pipeline recreation trigged by shader reloading (would need to ensure new
                    // postprocess_layout is not created...)
                    trigger_on_resize = true;

                    State::Complete {
                        pipelines,
                        shadow,
                        recreating: None,
                    }
                },
                Ok(Err(e)) => {
                    error!(?e, "Could not recreate shaders from assets due to an error");
                    State::Complete {
                        pipelines,
                        shadow,
                        recreating: None,
                    }
                },
                // Not complete
                Err(pipeline_creation) => State::Complete {
                    pipelines,
                    shadow,
                    recreating: Some((new_pipeline_modes, pipeline_creation)),
                },
            }
        } else {
            state
        };

        // Call on_resize to recreate render targets and their bind groups if the
        // pipelines were recreated with a new postprocess layout and or changes in the
        // render modes
        if trigger_on_resize {
            self.on_resize(self.resolution);
        }

        // If the shaders files were changed attempt to recreate the shaders
        if self.shaders_watcher.reloaded() {
            self.recreate_pipelines(self.pipeline_modes.clone());
        }

        // Or if we have a recreation pending
        if matches!(&self.state, State::Complete {
            recreating: None,
            ..
        }) {
            if let Some(new_pipeline_modes) = self.recreation_pending.take() {
                self.recreate_pipelines(new_pipeline_modes);
            }
        }

        let tex = match self.swap_chain.get_current_frame() {
            Ok(frame) => frame.output,
            // If lost recreate the swap chain
            Err(err @ wgpu::SwapChainError::Lost) => {
                warn!("{}. Recreating swap chain. A frame will be missed", err);
                self.on_resize(self.resolution);
                return Ok(None);
            },
            Err(wgpu::SwapChainError::Timeout) => {
                // This will probably be resolved on the next frame
                // NOTE: we don't log this because it happens very frequently with
                // PresentMode::Fifo and unlimited FPS on certain machines
                return Ok(None);
            },
            Err(err @ wgpu::SwapChainError::Outdated) => {
                warn!("{}. Recreating the swapchain", err);
                self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc);
                return Ok(None);
            },
            Err(err @ wgpu::SwapChainError::OutOfMemory) => return Err(err.into()),
        };
        let encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("A render encoder"),
            });

        Ok(Some(drawer::Drawer::new(encoder, self, tex, globals)))
    }

    /// Recreate the pipelines
    fn recreate_pipelines(&mut self, pipeline_modes: PipelineModes) {
        match &mut self.state {
            State::Complete { recreating, .. } if recreating.is_some() => {
                // Defer recreation so that we are not building multiple sets of pipelines in
                // the background at once
                self.recreation_pending = Some(pipeline_modes);
            },
            State::Complete {
                recreating, shadow, ..
            } => {
                *recreating = Some((
                    pipeline_modes.clone(),
                    pipeline_creation::recreate_pipelines(
                        Arc::clone(&self.device),
                        Arc::clone(&self.layouts.immutable),
                        self.shaders.cloned(),
                        pipeline_modes,
                        // NOTE: if present_mode starts to be used to configure pipelines then it
                        // needs to become a part of the pipeline modes
                        // (note here since the present mode is accessible
                        // through the swap chain descriptor)
                        self.sc_desc.clone(), // Note: cheap clone
                        shadow.map.is_enabled(),
                    ),
                ));
            },
            State::Interface { .. } => {
                // Defer recreation so that we are not building multiple sets of pipelines in
                // the background at once
                self.recreation_pending = Some(pipeline_modes);
            },
            State::Nothing => {},
        }
    }

    /// Create a new set of constants with the provided values.
    pub fn create_consts<T: Copy + bytemuck::Pod>(&mut self, vals: &[T]) -> Consts<T> {
        Self::create_consts_inner(&self.device, &self.queue, vals)
    }

    pub fn create_consts_inner<T: Copy + bytemuck::Pod>(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        vals: &[T],
    ) -> Consts<T> {
        let mut consts = Consts::new(device, vals.len());
        consts.update(queue, vals, 0);
        consts
    }

    /// Update a set of constants with the provided values.
    pub fn update_consts<T: Copy + bytemuck::Pod>(&self, consts: &mut Consts<T>, vals: &[T]) {
        consts.update(&self.queue, vals, 0)
    }

    pub fn update_clouds_locals(&mut self, new_val: clouds::Locals) {
        self.locals.clouds.update(&self.queue, &[new_val], 0)
    }

    pub fn update_postprocess_locals(&mut self, new_val: postprocess::Locals) {
        self.locals.postprocess.update(&self.queue, &[new_val], 0)
    }

    /// Create a new set of instances with the provided values.
    pub fn create_instances<T: Copy + bytemuck::Pod>(
        &mut self,
        vals: &[T],
    ) -> Result<Instances<T>, RenderError> {
        let mut instances = Instances::new(&self.device, vals.len());
        instances.update(&self.queue, vals, 0);
        Ok(instances)
    }

    /// Ensure that the quad index buffer is large enough for a quad vertex
    /// buffer with this many vertices
    pub(super) fn ensure_sufficient_index_length<V: Vertex>(
        &mut self,
        // Length of the vert buffer with 4 verts per quad
        vert_length: usize,
    ) {
        let quad_index_length = vert_length / 4 * 6;

        match V::QUADS_INDEX {
            Some(wgpu::IndexFormat::Uint16) => {
                // Make sure the global quad index buffer is large enough
                if self.quad_index_buffer_u16.len() < quad_index_length {
                    // Make sure we aren't over the max
                    if vert_length > u16::MAX as usize {
                        panic!(
                            "Vertex type: {} needs to use a larger index type, length: {}",
                            core::any::type_name::<V>(),
                            vert_length
                        );
                    }
                    self.quad_index_buffer_u16 =
                        create_quad_index_buffer_u16(&self.device, vert_length);
                }
            },
            Some(wgpu::IndexFormat::Uint32) => {
                // Make sure the global quad index buffer is large enough
                if self.quad_index_buffer_u32.len() < quad_index_length {
                    // Make sure we aren't over the max
                    if vert_length > u32::MAX as usize {
                        panic!(
                            "More than u32::MAX({}) verts({}) for type({}) using an index buffer!",
                            u32::MAX,
                            vert_length,
                            core::any::type_name::<V>()
                        );
                    }
                    self.quad_index_buffer_u32 =
                        create_quad_index_buffer_u32(&self.device, vert_length);
                }
            },
            None => {},
        }
    }

    pub fn create_sprite_verts(&mut self, mesh: Mesh<sprite::Vertex>) -> sprite::SpriteVerts {
        self.ensure_sufficient_index_length::<sprite::Vertex>(sprite::VERT_PAGE_SIZE as usize);
        sprite::create_verts_buffer(&self.device, mesh)
    }

    /// Create a new model from the provided mesh.
    /// If the provided mesh is empty this returns None
    pub fn create_model<V: Vertex>(&mut self, mesh: &Mesh<V>) -> Option<Model<V>> {
        self.ensure_sufficient_index_length::<V>(mesh.vertices().len());
        Model::new(&self.device, mesh)
    }

    /// Create a new dynamic model with the specified size.
    pub fn create_dynamic_model<V: Vertex>(&mut self, size: usize) -> DynamicModel<V> {
        self.ensure_sufficient_index_length::<V>(size);
        DynamicModel::new(&self.device, size)
    }

    /// Update a dynamic model with a mesh and a offset.
    pub fn update_model<V: Vertex>(&self, model: &DynamicModel<V>, mesh: &Mesh<V>, offset: usize) {
        model.update(&self.queue, mesh, offset)
    }

    /// Return the maximum supported texture size.
    pub fn max_texture_size(&self) -> u32 { Self::max_texture_size_raw(&self.device) }

    /// Return the maximum supported texture size from the factory.
    fn max_texture_size_raw(_device: &wgpu::Device) -> u32 {
        // This value is temporary as there are plans to include a way to get this in
        // wgpu this is just a sane standard for now
        8192
    }

    /// Create a new immutable texture from the provided image.
    /// # Panics
    /// If the provided data doesn't completely fill the texture this function
    /// will panic.
    pub fn create_texture_with_data_raw(
        &mut self,
        texture_info: &wgpu::TextureDescriptor,
        view_info: &wgpu::TextureViewDescriptor,
        sampler_info: &wgpu::SamplerDescriptor,
        data: &[u8],
    ) -> Texture {
        let tex = Texture::new_raw(&self.device, texture_info, view_info, sampler_info);

        let size = texture_info.size;
        let block_size = texture_info.format.describe().block_size;
        assert_eq!(
            size.width as usize
                * size.height as usize
                * size.depth_or_array_layers as usize
                * block_size as usize,
            data.len(),
            "Provided data length {} does not fill the provided texture size {:?}",
            data.len(),
            size,
        );

        tex.update(
            &self.queue,
            [0; 2],
            [texture_info.size.width, texture_info.size.height],
            data,
        );

        tex
    }

    /// Create a new raw texture.
    pub fn create_texture_raw(
        &mut self,
        texture_info: &wgpu::TextureDescriptor,
        view_info: &wgpu::TextureViewDescriptor,
        sampler_info: &wgpu::SamplerDescriptor,
    ) -> Texture {
        let texture = Texture::new_raw(&self.device, texture_info, view_info, sampler_info);
        texture.clear(&self.queue); // Needs to be fully initialized for partial writes to work on Dx12 AMD
        texture
    }

    /// Create a new texture from the provided image.
    ///
    /// Currently only supports Rgba8Srgb
    pub fn create_texture(
        &mut self,
        image: &image::DynamicImage,
        filter_method: Option<FilterMode>,
        address_mode: Option<AddressMode>,
    ) -> Result<Texture, RenderError> {
        Texture::new(
            &self.device,
            &self.queue,
            image,
            filter_method,
            address_mode,
        )
    }

    /// Create a new dynamic texture with the
    /// specified dimensions.
    ///
    /// Currently only supports Rgba8Srgb
    pub fn create_dynamic_texture(&mut self, dims: Vec2<u32>) -> Texture {
        Texture::new_dynamic(&self.device, &self.queue, dims.x, dims.y)
    }

    /// Update a texture with the provided offset, size, and data.
    ///
    /// Currently only supports Rgba8Srgb
    pub fn update_texture(
        &mut self,
        texture: &Texture, /* <T> */
        offset: [u32; 2],
        size: [u32; 2],
        // TODO: be generic over pixel type
        data: &[[u8; 4]],
    ) {
        texture.update(&self.queue, offset, size, bytemuck::cast_slice(data))
    }

    /// Queue to obtain a screenshot on the next frame render
    pub fn create_screenshot(
        &mut self,
        screenshot_handler: impl FnOnce(Result<image::DynamicImage, String>) + Send + 'static,
    ) {
        // Queue screenshot
        self.take_screenshot = Some(Box::new(screenshot_handler));
        // Take profiler snapshot
        if self.other_modes.profiler_enabled {
            let file_name = format!(
                "frame-trace_{}.json",
                std::time::SystemTime::now()
                    .duration_since(std::time::SystemTime::UNIX_EPOCH)
                    .map(|d| d.as_millis())
                    .unwrap_or(0)
            );

            if let Err(err) = wgpu_profiler::chrometrace::write_chrometrace(
                std::path::Path::new(&file_name),
                &self.profile_times,
            ) {
                error!(?err, "Failed to save GPU timing snapshot");
            } else {
                info!("Saved GPU timing snapshot as: {}", file_name);
            }
        }
    }

    // Consider reenabling at some time
    //
    // /// Queue the rendering of the player silhouette in the upcoming frame.
    // pub fn render_player_shadow(
    //     &mut self,
    //     _model: &figure::FigureModel,
    //     _col_lights: &Texture<ColLightFmt>,
    //     _global: &GlobalModel,
    //     _bones: &Consts<figure::BoneData>,
    //     _lod: &lod_terrain::LodData,
    //     _locals: &Consts<shadow::Locals>,
    // ) {
    //     // FIXME: Consider reenabling at some point.
    //     /* let (point_shadow_maps, directed_shadow_maps) =
    //         if let Some(shadow_map) = &mut self.shadow_map {
    //             (
    //                 (
    //                     shadow_map.point_res.clone(),
    //                     shadow_map.point_sampler.clone(),
    //                 ),
    //                 (
    //                     shadow_map.directed_res.clone(),
    //                     shadow_map.directed_sampler.clone(),
    //                 ),
    //             )
    //         } else {
    //             (
    //                 (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()),
    //                 (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()),
    //             )
    //         };
    //     let model = &model.opaque;

    //     self.encoder.draw(
    //         &gfx::Slice {
    //             start: model.vertex_range().start,
    //             end: model.vertex_range().end,
    //             base_vertex: 0,
    //             instances: None,
    //             buffer: gfx::IndexBuffer::Auto,
    //         },
    //         &self.player_shadow_pipeline.pso,
    //         &figure::pipe::Data {
    //             vbuf: model.vbuf.clone(),
    //             col_lights: (col_lights.srv.clone(), col_lights.sampler.clone()),
    //             locals: locals.buf.clone(),
    //             globals: global.globals.buf.clone(),
    //             bones: bones.buf.clone(),
    //             lights: global.lights.buf.clone(),
    //             shadows: global.shadows.buf.clone(),
    //             light_shadows: global.shadow_mats.buf.clone(),
    //             point_shadow_maps,
    //             directed_shadow_maps,
    //             noise: (self.noise_tex.srv.clone(),
    // self.noise_tex.sampler.clone()),             alt: (lod.alt.srv.clone(),
    // lod.alt.sampler.clone()),             horizon: (lod.horizon.srv.clone(),
    // lod.horizon.sampler.clone()),             tgt_color:
    // self.tgt_color_view.clone(),             tgt_depth:
    // (self.tgt_depth_view.clone()/* , (0, 0) */),         },
    //     ); */
    // }
}

fn create_quad_index_buffer_u16(device: &wgpu::Device, vert_length: usize) -> Buffer<u16> {
    assert!(vert_length <= u16::MAX as usize);
    let indices = [0, 1, 2, 2, 1, 3]
        .iter()
        .cycle()
        .copied()
        .take(vert_length / 4 * 6)
        .enumerate()
        .map(|(i, b)| (i / 6 * 4 + b) as u16)
        .collect::<Vec<_>>();

    Buffer::new(device, wgpu::BufferUsage::INDEX, &indices)
}

fn create_quad_index_buffer_u32(device: &wgpu::Device, vert_length: usize) -> Buffer<u32> {
    assert!(vert_length <= u32::MAX as usize);
    let indices = [0, 1, 2, 2, 1, 3]
        .iter()
        .cycle()
        .copied()
        .take(vert_length / 4 * 6)
        .enumerate()
        .map(|(i, b)| (i / 6 * 4 + b) as u32)
        .collect::<Vec<_>>();

    Buffer::new(device, wgpu::BufferUsage::INDEX, &indices)
}
