use super::{
    AlphaMode as SceneAlphaMode, BlinnPhongMaterial, Camera, Material, PbrMaterial, RenderError,
    RenderLimits, Renderable, Texture,
};
use bevy::{
    app::SubApps,
    asset::RenderAssetUsages,
    camera::{PerspectiveProjection, Projection, RenderTarget},
    image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor},
    material::AlphaMode,
    mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology},
    pbr::{ExtendedMaterial, MaterialExtension, MaterialPipeline, MaterialPipelineKey},
    prelude::*,
    reflect::TypePath,
    render::{
        RenderPlugin,
        render_resource::{
            AsBindGroup, Extent3d, PollType, RenderPipelineDescriptor, ShaderType,
            SpecializedMeshPipelineError, TextureDimension, TextureFormat, TextureUsages,
        },
        renderer::RenderDevice,
        view::screenshot::{Screenshot, ScreenshotCaptured},
    },
    shader::{Shader, ShaderRef},
    window::ExitCondition,
};
use std::{
    collections::HashMap,
    hash::{Hash, Hasher},
    marker::PhantomData,
    sync::{Mutex, mpsc},
    thread,
    time::{Duration, Instant},
};

const WORLD_AMBIENT_BRIGHTNESS: f32 = 400.0;
const WORLD_ENVIRONMENT_BRIGHTNESS: f32 = 600.0;

enum RenderCommand {
    Frame {
        camera: Camera,
        renderables: Vec<Renderable>,
        textures: Vec<Texture>,
        background_srgb: [u8; 4],
        limits: RenderLimits,
        reply: mpsc::SyncSender<Result<RenderedFrame, RenderError>>,
    },
    LoadScene {
        camera: Camera,
        renderables: std::sync::Arc<[Renderable]>,
        textures: std::sync::Arc<[Texture]>,
        limits: RenderLimits,
        reply: mpsc::SyncSender<Result<RenderTimings, RenderError>>,
    },
    View {
        camera: Camera,
        background_srgb: [u8; 4],
        limits: RenderLimits,
        reply: mpsc::SyncSender<Result<RenderedFrame, RenderError>>,
    },
    Stop,
}

/// Wall-clock costs of one renderer call. GPU work and readback are included in
/// `capture`; the fields before it are CPU-side preparation.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RenderTimings {
    pub validation: Duration,
    pub command_copy: Duration,
    pub clear_previous_frame: Duration,
    pub texture_cache_sync: Duration,
    pub mesh_cache_sync: Duration,
    pub scene_setup: Duration,
    pub capture: Duration,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RenderedFrame {
    pub rgba: Vec<u8>,
    pub timings: RenderTimings,
}

/// Reusable renderer facade. Bevy and its ECS stay on a dedicated OS thread.
pub struct Renderer {
    commands: mpsc::SyncSender<RenderCommand>,
    thread: Mutex<Option<thread::JoinHandle<()>>>,
}

impl Renderer {
    pub fn new_blocking() -> Result<Self, RenderError> {
        let (commands, receiver) = mpsc::sync_channel(1);
        let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
        let handle = thread::Builder::new()
            .name("metacrate-bevy-renderer".into())
            .spawn(move || {
                let backend = BevyBackend::new();
                match backend {
                    Ok(mut backend) => {
                        let _ = ready_sender.send(Ok(()));
                        while let Ok(command) = receiver.recv() {
                            match command {
                                RenderCommand::Frame {
                                    camera,
                                    renderables,
                                    textures,
                                    background_srgb,
                                    limits,
                                    reply,
                                } => {
                                    let _ = reply.send(backend.render(
                                        camera,
                                        &renderables,
                                        &textures,
                                        background_srgb,
                                        limits,
                                    ));
                                }
                                RenderCommand::LoadScene {
                                    camera,
                                    renderables,
                                    textures,
                                    limits,
                                    reply,
                                } => {
                                    let _ = reply.send(backend.load_scene(
                                        camera,
                                        &renderables,
                                        &textures,
                                        limits,
                                    ));
                                }
                                RenderCommand::View {
                                    camera,
                                    background_srgb,
                                    limits,
                                    reply,
                                } => {
                                    let _ = reply.send(backend.render_view(
                                        camera,
                                        background_srgb,
                                        limits,
                                    ));
                                }
                                RenderCommand::Stop => break,
                            }
                        }
                    }
                    Err(error) => {
                        let _ = ready_sender.send(Err(error));
                    }
                }
            })
            .map_err(|error| RenderError::Device(error.to_string()))?;
        ready_receiver
            .recv_timeout(Duration::from_secs(30))
            .map_err(|_| RenderError::TimedOut)??;
        Ok(Self {
            commands,
            thread: Mutex::new(Some(handle)),
        })
    }

    pub fn render(
        &self,
        camera: Camera,
        renderables: &[Renderable],
        textures: &[Texture],
        background_srgb: [u8; 4],
        limits: RenderLimits,
    ) -> Result<Vec<u8>, RenderError> {
        self.render_profiled(camera, renderables, textures, background_srgb, limits)
            .map(|frame| frame.rgba)
    }

    pub fn render_profiled(
        &self,
        camera: Camera,
        renderables: &[Renderable],
        textures: &[Texture],
        background_srgb: [u8; 4],
        limits: RenderLimits,
    ) -> Result<RenderedFrame, RenderError> {
        let started = Instant::now();
        validate_scene(camera, renderables, textures, limits)?;
        let validation = started.elapsed();
        let started = Instant::now();
        let (sender, receiver) = mpsc::sync_channel(1);
        let command = RenderCommand::Frame {
            camera,
            renderables: renderables.to_vec(),
            textures: textures.to_vec(),
            background_srgb,
            limits,
            reply: sender,
        };
        let command_copy = started.elapsed();
        self.commands
            .try_send(command)
            .map_err(|error| match error {
                mpsc::TrySendError::Full(_) => RenderError::TimedOut,
                mpsc::TrySendError::Disconnected(_) => {
                    RenderError::Device("renderer thread stopped".into())
                }
            })?;
        let mut frame = receiver
            .recv_timeout(Duration::from_mins(2))
            .map_err(|_| RenderError::TimedOut)??;
        frame.timings.validation = validation;
        frame.timings.command_copy = command_copy;
        Ok(frame)
    }

    pub fn load_scene(
        &self,
        camera: Camera,
        renderables: &[Renderable],
        textures: &[Texture],
        limits: RenderLimits,
    ) -> Result<RenderTimings, RenderError> {
        self.load_scene_shared(
            camera,
            renderables.to_vec().into(),
            textures.to_vec().into(),
            limits,
        )
    }

    pub fn load_scene_shared(
        &self,
        camera: Camera,
        renderables: std::sync::Arc<[Renderable]>,
        textures: std::sync::Arc<[Texture]>,
        limits: RenderLimits,
    ) -> Result<RenderTimings, RenderError> {
        let started = Instant::now();
        validate_scene(camera, &renderables, &textures, limits)?;
        let validation = started.elapsed();
        let started = Instant::now();
        let (sender, receiver) = mpsc::sync_channel(1);
        let command = RenderCommand::LoadScene {
            camera,
            renderables,
            textures,
            limits,
            reply: sender,
        };
        let command_copy = started.elapsed();
        self.send(command)?;
        let mut timings = receiver
            .recv_timeout(Duration::from_mins(2))
            .map_err(|_| RenderError::TimedOut)??;
        timings.validation = validation;
        timings.command_copy = command_copy;
        Ok(timings)
    }

    pub fn render_view(
        &self,
        camera: Camera,
        background_srgb: [u8; 4],
        limits: RenderLimits,
    ) -> Result<RenderedFrame, RenderError> {
        camera.basis()?;
        if limits.width == 0 || limits.height == 0 || limits.far_distance == 0 {
            return Err(RenderError::InvalidScene);
        }
        let (sender, receiver) = mpsc::sync_channel(1);
        self.send(RenderCommand::View {
            camera,
            background_srgb,
            limits,
            reply: sender,
        })?;
        receiver
            .recv_timeout(Duration::from_mins(2))
            .map_err(|_| RenderError::TimedOut)?
    }

    fn send(&self, command: RenderCommand) -> Result<(), RenderError> {
        self.commands
            .try_send(command)
            .map_err(|error| match error {
                mpsc::TrySendError::Full(_) => RenderError::TimedOut,
                mpsc::TrySendError::Disconnected(_) => {
                    RenderError::Device("renderer thread stopped".into())
                }
            })
    }
}

impl Drop for Renderer {
    fn drop(&mut self) {
        let stop_accepted = self.commands.try_send(RenderCommand::Stop).is_ok();
        let handle = self
            .thread
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .take();
        if stop_accepted && let Some(handle) = handle {
            let _ = handle.join();
        }
    }
}

#[derive(Component)]
struct FrameEntity;

#[derive(Component)]
struct PersistentSceneEntity;

struct LegacyMaterialPlugin;
struct PbrMaterialPlugin;

const LEGACY_SHADER_HANDLE: Handle<Shader> = Handle::Uuid(
    bevy::asset::uuid::Uuid::from_u128(0x1b31_1fe4_6b1b_47e1_9a4b_1018_c8e7_094f),
    PhantomData,
);
const PBR_SHADER_HANDLE: Handle<Shader> = Handle::Uuid(
    bevy::asset::uuid::Uuid::from_u128(0xb5c9_1d3a_a566_449f_b864_b3c0_d5f9_e684),
    PhantomData,
);

impl Plugin for LegacyMaterialPlugin {
    fn build(&self, app: &mut App) {
        bevy::asset::load_internal_asset!(
            app,
            LEGACY_SHADER_HANDLE,
            "legacy_material.wgsl",
            Shader::from_wgsl
        );
        app.add_plugins(MaterialPlugin::<LegacyBevyMaterial>::default());
    }
}

impl Plugin for PbrMaterialPlugin {
    fn build(&self, app: &mut App) {
        bevy::asset::load_internal_asset!(
            app,
            PBR_SHADER_HANDLE,
            "pbr_material.wgsl",
            Shader::from_wgsl
        );
        app.add_plugins(MaterialPlugin::<PbrBevyMaterial>::default());
    }
}

#[derive(Clone, Copy, Debug, ShaderType)]
struct LegacyUniform {
    diffuse_color: LinearRgba,
    specular_color: LinearRgba,
    diffuse_scale_offset: Vec4,
    normal_scale_offset: Vec4,
    specular_scale_offset: Vec4,
    rotations: Vec4,
    surface: Vec4,
    camera_position: Vec4,
}

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct LegacyBevyMaterial {
    #[uniform(0)]
    uniform: LegacyUniform,
    #[texture(1)]
    #[sampler(2)]
    diffuse: Option<Handle<Image>>,
    #[texture(3)]
    #[sampler(4)]
    normal: Option<Handle<Image>>,
    #[texture(5)]
    #[sampler(6)]
    specular: Option<Handle<Image>>,
    alpha_mode: AlphaMode,
}

impl bevy::pbr::Material for LegacyBevyMaterial {
    fn fragment_shader() -> ShaderRef {
        LEGACY_SHADER_HANDLE.into()
    }

    fn alpha_mode(&self) -> AlphaMode {
        self.alpha_mode
    }

    fn specialize(
        _pipeline: &MaterialPipeline,
        descriptor: &mut RenderPipelineDescriptor,
        _layout: &MeshVertexBufferLayoutRef,
        _key: MaterialPipelineKey<Self>,
    ) -> Result<(), SpecializedMeshPipelineError> {
        descriptor.primitive.cull_mode = None;
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, ShaderType)]
struct PbrUniform {
    base_scale_offset: Vec4,
    normal_scale_offset: Vec4,
    metallic_roughness_scale_offset: Vec4,
    emissive_scale_offset: Vec4,
    rotations: Vec4,
    maps: Vec4,
}

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct PbrExtension {
    #[uniform(100)]
    uniform: PbrUniform,
    #[texture(101)]
    #[sampler(102)]
    base_color: Option<Handle<Image>>,
    #[texture(103)]
    #[sampler(104)]
    normal: Option<Handle<Image>>,
    #[texture(105)]
    #[sampler(106)]
    metallic_roughness: Option<Handle<Image>>,
    #[texture(107)]
    #[sampler(108)]
    emissive: Option<Handle<Image>>,
}

impl MaterialExtension for PbrExtension {
    fn fragment_shader() -> ShaderRef {
        PBR_SHADER_HANDLE.into()
    }
}

type PbrBevyMaterial = ExtendedMaterial<StandardMaterial, PbrExtension>;

#[derive(Clone)]
struct GpuTexture {
    srgb: Handle<Image>,
    linear: Handle<Image>,
}

struct CachedGpuTexture {
    source: Texture,
    gpu: GpuTexture,
}

struct CachedGpuMesh {
    source: super::Mesh,
    gpu: Handle<Mesh>,
}

struct BevyBackend {
    apps: SubApps,
    frame_entities: Vec<Entity>,
    scene_entities: Vec<Entity>,
    texture_handles: Vec<GpuTexture>,
    texture_cache: HashMap<u64, Vec<CachedGpuTexture>>,
    mesh_cache: HashMap<u64, Vec<CachedGpuMesh>>,
    pbr_handles: Vec<Handle<PbrBevyMaterial>>,
    legacy_handles: Vec<Handle<LegacyBevyMaterial>>,
    environment_map: EnvironmentMapLight,
    target: Option<(u32, u32, Handle<Image>)>,
}

impl BevyBackend {
    fn new() -> Result<Self, RenderError> {
        let render_plugin = RenderPlugin {
            synchronous_pipeline_compilation: true,
            ..default()
        };
        let window_plugin = WindowPlugin {
            primary_window: None,
            exit_condition: ExitCondition::DontExit,
            ..default()
        };
        let mut app = App::new();
        let plugins = DefaultPlugins
            .set(window_plugin)
            .set(render_plugin)
            .set(ImagePlugin::default_linear());
        #[cfg(feature = "windowed")]
        let plugins = plugins.disable::<bevy::winit::WinitPlugin>();
        app.add_plugins(plugins)
            .add_plugins((LegacyMaterialPlugin, PbrMaterialPlugin));
        app.insert_resource(GlobalAmbientLight {
            color: Color::WHITE,
            brightness: WORLD_AMBIENT_BRIGHTNESS,
            ..default()
        });
        let environment_map = {
            let mut images = app.world_mut().resource_mut::<Assets<Image>>();
            let mut light = EnvironmentMapLight::hemispherical_gradient(
                &mut images,
                Color::srgb_u8(150, 180, 220),
                Color::srgb_u8(175, 175, 165),
                Color::srgb_u8(95, 90, 70),
            );
            light.intensity = WORLD_ENVIRONMENT_BRIGHTNESS;
            light
        };
        app.finish();
        app.cleanup();
        if !app.world().contains_resource::<RenderDevice>() {
            return Err(RenderError::AdapterUnavailable);
        }
        Ok(Self {
            apps: std::mem::take(app.sub_apps_mut()),
            frame_entities: Vec::new(),
            scene_entities: Vec::new(),
            texture_handles: Vec::new(),
            texture_cache: HashMap::new(),
            mesh_cache: HashMap::new(),
            pbr_handles: Vec::new(),
            legacy_handles: Vec::new(),
            environment_map,
            target: None,
        })
    }

    #[allow(clippy::too_many_lines)]
    fn render(
        &mut self,
        camera: Camera,
        renderables: &[Renderable],
        textures: &[Texture],
        background_srgb: [u8; 4],
        limits: RenderLimits,
    ) -> Result<RenderedFrame, RenderError> {
        let mut timings = self.load_scene(camera, renderables, textures, limits)?;
        let frame = self.render_view(camera, background_srgb, limits)?;
        timings.clear_previous_frame += frame.timings.clear_previous_frame;
        timings.scene_setup += frame.timings.scene_setup;
        timings.capture += frame.timings.capture;
        Ok(RenderedFrame {
            rgba: frame.rgba,
            timings,
        })
    }

    #[allow(clippy::too_many_lines)]
    fn load_scene(
        &mut self,
        camera: Camera,
        renderables: &[Renderable],
        textures: &[Texture],
        _limits: RenderLimits,
    ) -> Result<RenderTimings, RenderError> {
        let started = Instant::now();
        self.clear_scene();
        let clear_previous_frame = started.elapsed();
        let started = Instant::now();
        self.texture_handles = add_textures(
            self.apps.main.world_mut(),
            textures,
            &mut self.texture_cache,
        );
        let texture_cache_sync = started.elapsed();
        let started = Instant::now();
        let mesh_handles = add_meshes(
            self.apps.main.world_mut(),
            renderables,
            &mut self.mesh_cache,
        )?;
        let mesh_cache_sync = started.elapsed();
        let started = Instant::now();
        for (renderable, mesh) in renderables.iter().zip(mesh_handles) {
            let entity = match &renderable.material {
                Material::BlinnPhong(material) => {
                    let material = self
                        .apps
                        .main
                        .world_mut()
                        .resource_mut::<Assets<LegacyBevyMaterial>>()
                        .add(legacy_as_bevy(material, &self.texture_handles, camera));
                    let entity = self
                        .apps
                        .main
                        .world_mut()
                        .spawn((
                            Mesh3d(mesh.clone()),
                            MeshMaterial3d(material.clone()),
                            PersistentSceneEntity,
                        ))
                        .id();
                    self.legacy_handles.push(material);
                    entity
                }
                Material::Pbr(material) => {
                    let material = self
                        .apps
                        .main
                        .world_mut()
                        .resource_mut::<Assets<PbrBevyMaterial>>()
                        .add(pbr_as_bevy(material, &self.texture_handles));
                    let entity = self
                        .apps
                        .main
                        .world_mut()
                        .spawn((
                            Mesh3d(mesh.clone()),
                            MeshMaterial3d(material.clone()),
                            PersistentSceneEntity,
                        ))
                        .id();
                    self.pbr_handles.push(material);
                    entity
                }
            };
            self.scene_entities.push(entity);
        }
        let light = self
            .apps
            .main
            .world_mut()
            .spawn((
                DirectionalLight {
                    illuminance: 12_000.0,
                    shadow_maps_enabled: false,
                    ..Default::default()
                },
                Transform::from_xyz(40.0, 80.0, 30.0).looking_at(Vec3::ZERO, Vec3::Y),
                PersistentSceneEntity,
            ))
            .id();
        self.scene_entities.push(light);
        Ok(RenderTimings {
            clear_previous_frame,
            texture_cache_sync,
            mesh_cache_sync,
            scene_setup: started.elapsed(),
            ..Default::default()
        })
    }

    fn render_view(
        &mut self,
        camera: Camera,
        background_srgb: [u8; 4],
        limits: RenderLimits,
    ) -> Result<RenderedFrame, RenderError> {
        let started = Instant::now();
        self.clear_frame();
        let clear_previous_frame = started.elapsed();
        self.apps
            .main
            .world_mut()
            .insert_resource(ClearColor(Color::srgba_u8(
                background_srgb[0],
                background_srgb[1],
                background_srgb[2],
                background_srgb[3],
            )));
        let camera_position = world_vector(camera.position).extend(1.0);
        for handle in &self.legacy_handles {
            if let Some(mut material) = self
                .apps
                .main
                .world_mut()
                .resource_mut::<Assets<LegacyBevyMaterial>>()
                .get_mut(handle)
            {
                material.uniform.camera_position = camera_position;
            }
        }
        let started = Instant::now();
        let target = self.new_target(limits.width, limits.height);
        let transform = camera_transform(camera)?;
        let camera_entity = self
            .apps
            .main
            .world_mut()
            .spawn((
                Camera3d::default(),
                target.clone(),
                Projection::Perspective(PerspectiveProjection {
                    fov: camera.vertical_fov_degrees.to_radians(),
                    near: 0.05,
                    #[allow(clippy::cast_precision_loss)]
                    far: limits.far_distance as f32,
                    ..Default::default()
                }),
                transform,
                self.environment_map.clone(),
                FrameEntity,
            ))
            .id();
        self.frame_entities.push(camera_entity);
        let scene_setup = started.elapsed();
        let started = Instant::now();
        let rgba = self.capture(&target)?;
        Ok(RenderedFrame {
            rgba,
            timings: RenderTimings {
                clear_previous_frame,
                scene_setup,
                capture: started.elapsed(),
                ..Default::default()
            },
        })
    }

    fn new_target(&mut self, width: u32, height: u32) -> RenderTarget {
        if let Some((old_width, old_height, handle)) = &self.target
            && (*old_width, *old_height) == (width, height)
        {
            return handle.clone().into();
        }
        if let Some((_, _, old)) = self.target.take() {
            self.apps
                .main
                .world_mut()
                .resource_mut::<Assets<Image>>()
                .remove(&old);
        }
        let mut image = Image::new_uninit(
            Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            TextureDimension::D2,
            TextureFormat::Rgba8UnormSrgb,
            RenderAssetUsages::RENDER_WORLD,
        );
        image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
        let handle = self
            .apps
            .main
            .world_mut()
            .resource_mut::<Assets<Image>>()
            .add(image);
        self.target = Some((width, height, handle.clone()));
        handle.into()
    }

    fn capture(&mut self, target: &RenderTarget) -> Result<Vec<u8>, RenderError> {
        for _ in 0..2 {
            self.update_and_wait()?;
        }
        let image = target.as_image().ok_or(RenderError::InvalidScene)?.clone();
        let (sender, receiver) = mpsc::sync_channel(1);
        let screenshot = self
            .apps
            .main
            .world_mut()
            .spawn(Screenshot::image(image))
            .observe(move |event: On<ScreenshotCaptured>| {
                if let Some(bytes) = &event.image.data {
                    let _ = sender.try_send(bytes.clone());
                }
            })
            .id();
        self.frame_entities.push(screenshot);
        for _ in 0..8 {
            self.update_and_wait()?;
            if let Ok(bytes) = receiver.try_recv() {
                return Ok(bytes);
            }
        }
        Err(RenderError::Readback)
    }

    fn update_and_wait(&mut self) -> Result<(), RenderError> {
        self.apps.update();
        self.apps
            .main
            .world()
            .resource::<RenderDevice>()
            .wgpu_device()
            .poll(PollType::Wait {
                submission_index: None,
                timeout: Some(Duration::from_secs(10)),
            })
            .map(|_| ())
            .map_err(|_| RenderError::TimedOut)
    }

    fn clear_frame(&mut self) {
        for entity in self.frame_entities.drain(..) {
            let _ = self.apps.main.world_mut().despawn(entity);
        }
    }

    fn clear_scene(&mut self) {
        self.clear_frame();
        for entity in self.scene_entities.drain(..) {
            let _ = self.apps.main.world_mut().despawn(entity);
        }
        let world = self.apps.main.world_mut();
        self.texture_handles.clear();
        let mut materials = world.resource_mut::<Assets<PbrBevyMaterial>>();
        for handle in self.pbr_handles.drain(..) {
            materials.remove(&handle);
        }
        let mut materials = world.resource_mut::<Assets<LegacyBevyMaterial>>();
        for handle in self.legacy_handles.drain(..) {
            materials.remove(&handle);
        }
    }
}

fn add_textures(
    world: &mut World,
    textures: &[Texture],
    cache: &mut HashMap<u64, Vec<CachedGpuTexture>>,
) -> Vec<GpuTexture> {
    let mut assets = world.resource_mut::<Assets<Image>>();
    let mut next = HashMap::with_capacity(textures.len());
    let handles = textures
        .iter()
        .map(|texture| {
            let key = texture_key(texture);
            if let Some(cached) = next.get(&key).and_then(|bucket: &Vec<CachedGpuTexture>| {
                bucket.iter().find(|cached| cached.source == *texture)
            }) {
                return cached.gpu.clone();
            }
            if let Some(cached) = cache.get_mut(&key).and_then(|bucket| {
                bucket
                    .iter()
                    .position(|cached| cached.source == *texture)
                    .map(|position| bucket.swap_remove(position))
            }) {
                let gpu = cached.gpu.clone();
                next.entry(key).or_default().push(cached);
                return gpu;
            }
            let (rgba, mip_level_count) = rgba_mip_chain(texture);
            let image = |format| {
                let mut image = Image::new_uninit(
                    Extent3d {
                        width: texture.width,
                        height: texture.height,
                        depth_or_array_layers: 1,
                    },
                    TextureDimension::D2,
                    format,
                    RenderAssetUsages::RENDER_WORLD,
                );
                image.data = Some(rgba.clone());
                image.texture_descriptor.mip_level_count = mip_level_count;
                image
            };
            let sampler = || {
                ImageSampler::Descriptor(ImageSamplerDescriptor {
                    address_mode_u: ImageAddressMode::Repeat,
                    address_mode_v: ImageAddressMode::Repeat,
                    address_mode_w: ImageAddressMode::Repeat,
                    mag_filter: ImageFilterMode::Linear,
                    min_filter: ImageFilterMode::Linear,
                    mipmap_filter: ImageFilterMode::Linear,
                    ..Default::default()
                })
            };
            let mut srgb = image(TextureFormat::Rgba8UnormSrgb);
            srgb.sampler = sampler();
            let mut linear = image(TextureFormat::Rgba8Unorm);
            linear.sampler = sampler();
            let gpu = GpuTexture {
                srgb: assets.add(srgb),
                linear: assets.add(linear),
            };
            next.entry(key).or_default().push(CachedGpuTexture {
                source: texture.clone(),
                gpu: gpu.clone(),
            });
            gpu
        })
        .collect();
    for bucket in cache.drain().map(|(_, bucket)| bucket) {
        for cached in bucket {
            assets.remove(&cached.gpu.srgb);
            assets.remove(&cached.gpu.linear);
        }
    }
    *cache = next;
    handles
}

fn texture_key(texture: &Texture) -> u64 {
    let mut hash = std::collections::hash_map::DefaultHasher::new();
    texture.width.hash(&mut hash);
    texture.height.hash(&mut hash);
    texture.rgba.hash(&mut hash);
    hash.finish()
}

fn rgba_mip_chain(texture: &Texture) -> (Vec<u8>, u32) {
    let mut width = texture.width as usize;
    let mut height = texture.height as usize;
    let mut level = texture.rgba.clone();
    let mut output = level.clone();
    let mut levels = 1_u32;
    while width > 1 || height > 1 {
        let next_width = width.div_ceil(2);
        let next_height = height.div_ceil(2);
        let mut next = vec![0_u8; next_width * next_height * 4];
        for y in 0..next_height {
            for x in 0..next_width {
                let mut weighted_rgb = [0_u32; 3];
                let mut alpha = 0_u32;
                let mut samples = 0_u32;
                for source_y in y * 2..(y * 2 + 2).min(height) {
                    for source_x in x * 2..(x * 2 + 2).min(width) {
                        let source = (source_y * width + source_x) * 4;
                        let sample_alpha = u32::from(level[source + 3]);
                        alpha += sample_alpha;
                        for channel in 0..3 {
                            weighted_rgb[channel] +=
                                u32::from(level[source + channel]) * sample_alpha;
                        }
                        samples += 1;
                    }
                }
                let target = (y * next_width + x) * 4;
                for channel in 0..3 {
                    next[target + channel] =
                        u8::try_from(weighted_rgb[channel].checked_div(alpha).unwrap_or(0))
                            .expect("the alpha-weighted average of bytes fits in a byte");
                }
                next[target + 3] =
                    u8::try_from(alpha / samples).expect("the average of bytes fits in a byte");
            }
        }
        output.extend_from_slice(&next);
        level = next;
        width = next_width;
        height = next_height;
        levels += 1;
    }
    (output, levels)
}

fn add_mesh(world: &mut World, renderable: &Renderable) -> Result<Handle<Mesh>, RenderError> {
    let positions = renderable
        .mesh
        .vertices
        .iter()
        .map(|vertex| world_vector(vertex.position).to_array())
        .collect::<Vec<_>>();
    let normals = renderable
        .mesh
        .vertices
        .iter()
        .map(|vertex| world_vector(vertex.normal).normalize_or_zero().to_array())
        .collect::<Vec<_>>();
    let tex_coords = renderable
        .mesh
        .vertices
        .iter()
        .map(|vertex| vertex.tex_coord)
        .collect::<Vec<_>>();
    let colors = renderable
        .mesh
        .vertices
        .iter()
        .map(|vertex| scene_color(vertex.color_srgb).to_linear().to_f32_array())
        .collect::<Vec<_>>();
    let mut mesh = Mesh::new(
        PrimitiveTopology::TriangleList,
        RenderAssetUsages::RENDER_WORLD,
    )
    .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
    .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
    .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, tex_coords)
    .with_inserted_attribute(Mesh::ATTRIBUTE_COLOR, colors)
    .with_inserted_indices(Indices::U32(renderable.mesh.indices.clone()));
    mesh.generate_tangents()
        .map_err(|_| RenderError::InvalidScene)?;
    Ok(world.resource_mut::<Assets<Mesh>>().add(mesh))
}

fn add_meshes(
    world: &mut World,
    renderables: &[Renderable],
    cache: &mut HashMap<u64, Vec<CachedGpuMesh>>,
) -> Result<Vec<Handle<Mesh>>, RenderError> {
    let mut next = HashMap::with_capacity(renderables.len());
    let mut handles = Vec::with_capacity(renderables.len());
    for renderable in renderables {
        let key = mesh_key(&renderable.mesh);
        if let Some(cached) = next.get(&key).and_then(|bucket: &Vec<CachedGpuMesh>| {
            bucket
                .iter()
                .find(|cached| cached.source == renderable.mesh)
        }) {
            handles.push(cached.gpu.clone());
            continue;
        }
        if let Some(cached) = cache.get_mut(&key).and_then(|bucket| {
            bucket
                .iter()
                .position(|cached| cached.source == renderable.mesh)
                .map(|position| bucket.swap_remove(position))
        }) {
            handles.push(cached.gpu.clone());
            next.entry(key).or_default().push(cached);
            continue;
        }
        let gpu = add_mesh(world, renderable)?;
        handles.push(gpu.clone());
        next.entry(key).or_default().push(CachedGpuMesh {
            source: renderable.mesh.clone(),
            gpu,
        });
    }
    let mut assets = world.resource_mut::<Assets<Mesh>>();
    for bucket in cache.drain().map(|(_, bucket)| bucket) {
        for cached in bucket {
            assets.remove(&cached.gpu);
        }
    }
    *cache = next;
    Ok(handles)
}

fn mesh_key(mesh: &super::Mesh) -> u64 {
    let mut hash = std::collections::hash_map::DefaultHasher::new();
    for vertex in &mesh.vertices {
        for value in vertex
            .position
            .iter()
            .chain(&vertex.normal)
            .chain(&vertex.tex_coord)
        {
            value.to_bits().hash(&mut hash);
        }
        vertex.color_srgb.hash(&mut hash);
    }
    mesh.indices.hash(&mut hash);
    hash.finish()
}

fn legacy_as_bevy(
    material: &BlinnPhongMaterial,
    textures: &[GpuTexture],
    camera: Camera,
) -> LegacyBevyMaterial {
    let transform = |slot: super::TextureSlot| {
        Vec4::new(
            slot.transform.scale[0],
            slot.transform.scale[1],
            slot.transform.offset[0],
            slot.transform.offset[1],
        )
    };
    let cutoff = material.alpha_cutoff.map_or_else(
        || match material.alpha_mode {
            SceneAlphaMode::Mask { cutoff } => cutoff.clamp(0.0, 1.0),
            _ => -1.0,
        },
        |cutoff| cutoff.clamp(0.0, 1.0),
    );
    LegacyBevyMaterial {
        uniform: LegacyUniform {
            diffuse_color: scene_color(material.diffuse_color_srgb).to_linear(),
            specular_color: Color::srgb_u8(
                material.specular_color_srgb[0],
                material.specular_color_srgb[1],
                material.specular_color_srgb[2],
            )
            .to_linear(),
            diffuse_scale_offset: transform(material.diffuse),
            normal_scale_offset: transform(material.normal),
            specular_scale_offset: transform(material.specular),
            rotations: Vec4::new(
                material.diffuse.transform.rotation_radians,
                material.normal.transform.rotation_radians,
                material.specular.transform.rotation_radians,
                material.shininess.max(1.0),
            ),
            surface: Vec4::new(
                material.environment_intensity.clamp(0.0, 1.0),
                if material.fullbright { 1.0 } else { 0.0 },
                if material.normal.texture.is_some() {
                    1.0
                } else {
                    0.0
                },
                cutoff,
            ),
            camera_position: world_vector(camera.position).extend(1.0),
        },
        diffuse: srgb_texture(material.diffuse.texture, textures),
        normal: linear_texture(material.normal.texture, textures),
        specular: srgb_texture(material.specular.texture, textures),
        alpha_mode: alpha_mode(material.alpha_mode),
    }
}

fn pbr_as_bevy(material: &PbrMaterial, textures: &[GpuTexture]) -> PbrBevyMaterial {
    let transform = |slot: super::TextureSlot| {
        Vec4::new(
            slot.transform.scale[0],
            slot.transform.scale[1],
            slot.transform.offset[0],
            slot.transform.offset[1],
        )
    };
    PbrBevyMaterial {
        base: StandardMaterial {
            base_color: scene_color(material.base_color_srgb),
            metallic: material.metallic_factor.clamp(0.0, 1.0),
            perceptual_roughness: material.roughness_factor.clamp(0.04, 1.0),
            emissive: Color::srgb_u8(
                material.emissive_factor_srgb[0],
                material.emissive_factor_srgb[1],
                material.emissive_factor_srgb[2],
            )
            .to_linear(),
            double_sided: material.double_sided,
            cull_mode: (!material.double_sided)
                .then_some(bevy::render::render_resource::Face::Back),
            alpha_mode: alpha_mode(material.alpha_mode),
            ..Default::default()
        },
        extension: PbrExtension {
            uniform: PbrUniform {
                base_scale_offset: transform(material.base_color),
                normal_scale_offset: transform(material.normal),
                metallic_roughness_scale_offset: transform(material.metallic_roughness),
                emissive_scale_offset: transform(material.emissive),
                rotations: Vec4::new(
                    material.base_color.transform.rotation_radians,
                    material.normal.transform.rotation_radians,
                    material.metallic_roughness.transform.rotation_radians,
                    material.emissive.transform.rotation_radians,
                ),
                maps: Vec4::new(
                    bool_float(material.base_color.texture.is_some()),
                    bool_float(material.normal.texture.is_some()),
                    bool_float(material.metallic_roughness.texture.is_some()),
                    bool_float(material.emissive.texture.is_some()),
                ),
            },
            base_color: srgb_texture(material.base_color.texture, textures),
            normal: linear_texture(material.normal.texture, textures),
            metallic_roughness: linear_texture(material.metallic_roughness.texture, textures),
            emissive: srgb_texture(material.emissive.texture, textures),
        },
    }
}

fn srgb_texture(index: Option<usize>, textures: &[GpuTexture]) -> Option<Handle<Image>> {
    index.and_then(|index| textures.get(index).map(|texture| texture.srgb.clone()))
}

fn linear_texture(index: Option<usize>, textures: &[GpuTexture]) -> Option<Handle<Image>> {
    index.and_then(|index| textures.get(index).map(|texture| texture.linear.clone()))
}

const fn bool_float(value: bool) -> f32 {
    if value { 1.0 } else { 0.0 }
}

fn scene_color(color: [u8; 4]) -> Color {
    Color::srgba_u8(color[0], color[1], color[2], color[3])
}

fn alpha_mode(mode: SceneAlphaMode) -> AlphaMode {
    match mode {
        SceneAlphaMode::Opaque => AlphaMode::Opaque,
        SceneAlphaMode::Mask { cutoff } => AlphaMode::Mask(cutoff.clamp(0.0, 1.0)),
        SceneAlphaMode::Blend => AlphaMode::Blend,
    }
}

fn camera_transform(camera: Camera) -> Result<Transform, RenderError> {
    let forward = world_vector(camera.forward).normalize_or_zero();
    let up = world_vector(camera.up).normalize_or_zero();
    if forward.length_squared() < 0.5 || up.length_squared() < 0.5 {
        return Err(RenderError::InvalidScene);
    }
    Ok(Transform::from_translation(world_vector(camera.position)).looking_to(forward, up))
}

fn world_vector(value: [f32; 3]) -> Vec3 {
    Vec3::new(value[0], value[2], -value[1])
}

fn validate_scene(
    camera: Camera,
    renderables: &[Renderable],
    textures: &[Texture],
    limits: RenderLimits,
) -> Result<(), RenderError> {
    camera.basis()?;
    if limits.width == 0 || limits.height == 0 || limits.far_distance == 0 {
        return Err(RenderError::InvalidScene);
    }
    let triangle_count = renderables
        .iter()
        .try_fold(0usize, |total, renderable| {
            total.checked_add(renderable.mesh.indices.len() / 3)
        })
        .ok_or(RenderError::ResourceLimit)?;
    let (texture_bytes, texture_pixels) =
        textures
            .iter()
            .try_fold((0usize, 0usize), |(bytes, pixels), texture| {
                let count = usize::try_from(texture.width)
                    .ok()
                    .and_then(|width| {
                        usize::try_from(texture.height)
                            .ok()
                            .and_then(|height| width.checked_mul(height))
                    })
                    .ok_or(RenderError::ResourceLimit)?;
                if count == 0 || texture.rgba.len() != count.saturating_mul(4) {
                    return Err(RenderError::InvalidScene);
                }
                Ok((
                    bytes
                        .checked_add(texture.rgba.len())
                        .ok_or(RenderError::ResourceLimit)?,
                    pixels
                        .checked_add(count)
                        .ok_or(RenderError::ResourceLimit)?,
                ))
            })?;
    if triangle_count > limits.max_triangles
        || texture_bytes > limits.max_texture_bytes
        || texture_pixels > limits.max_texture_pixels
    {
        return Err(RenderError::ResourceLimit);
    }
    if renderables.iter().any(|renderable| {
        renderable.mesh.indices.iter().any(|index| {
            usize::try_from(*index).map_or(true, |index| index >= renderable.mesh.vertices.len())
        }) || renderable.mesh.vertices.iter().any(|vertex| {
            vertex
                .position
                .iter()
                .chain(vertex.normal.iter())
                .chain(vertex.tex_coord.iter())
                .any(|value| !value.is_finite())
        })
    }) {
        return Err(RenderError::InvalidScene);
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn shaders_use_their_native_texture_transform_conventions() {
        let legacy = include_str!("legacy_material.wgsl");
        assert!(legacy.contains("(centered.x * cosine + centered.y * sine) * scale_offset.x"));
        assert!(legacy.contains("(-centered.x * sine + centered.y * cosine) * scale_offset.y"));
        assert!(legacy.contains("vec2(uv.x, 1.0 - uv.y)"));
        assert!(legacy.contains("texel.a < material.surface.w"));

        let pbr = include_str!("pbr_material.wgsl");
        assert!(pbr.contains("let scaled = uv * scale_offset.xy"));
        assert!(pbr.contains("scaled.x * cosine - scaled.y * sine"));
        assert!(pbr.contains("scaled.x * sine + scaled.y * cosine"));
    }
    use crate::{Mesh as SceneMesh, TextureSlot, UvTransform, Vertex};
    use std::sync::Arc;

    #[test]
    fn mip_chain_reaches_one_pixel_and_averages_rgba() {
        let texture = Texture {
            width: 2,
            height: 2,
            rgba: vec![
                0, 0, 0, 255, 100, 100, 100, 255, 200, 200, 200, 255, 255, 255, 255, 255,
            ],
        };
        let (mips, levels) = rgba_mip_chain(&texture);
        assert_eq!(levels, 2);
        assert_eq!(mips.len(), 20);
        assert_eq!(&mips[16..], &[138, 138, 138, 255]);

        let cutout = Texture {
            width: 2,
            height: 2,
            rgba: vec![255, 220, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        };
        let (mips, _) = rgba_mip_chain(&cutout);
        assert_eq!(&mips[16..], &[255, 220, 0, 63]);
    }

    #[test]
    #[allow(clippy::too_many_lines)] // One public render sequence checks reuse and orientation.
    fn bevy_headless_renders_repeated_frames_through_the_public_api() {
        let renderer = match Renderer::new_blocking() {
            Ok(renderer) => Arc::new(renderer),
            Err(RenderError::AdapterUnavailable | RenderError::Device(_)) => return,
            Err(error) => panic!("unexpected renderer initialization failure: {error}"),
        };
        let camera = Camera::look_at([0.0; 3], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], 60.0).unwrap();
        let frames = [
            renderable(Material::BlinnPhong(BlinnPhongMaterial {
                diffuse_color_srgb: [255, 20, 20, 255],
                diffuse: TextureSlot {
                    texture: Some(0),
                    ..Default::default()
                },
                normal: TextureSlot {
                    texture: Some(1),
                    ..Default::default()
                },
                specular: TextureSlot {
                    texture: Some(0),
                    ..Default::default()
                },
                fullbright: true,
                double_sided: true,
                ..Default::default()
            })),
            renderable(Material::Pbr(PbrMaterial {
                base_color_srgb: [20, 40, 255, 255],
                base_color: texture_slot(0, 0.2),
                normal: texture_slot(1, -0.2),
                metallic_roughness: texture_slot(2, 0.3),
                emissive: texture_slot(3, -0.3),
                metallic_factor: 0.0,
                roughness_factor: 0.7,
                double_sided: true,
                ..Default::default()
            })),
        ];
        let textures = [
            solid_texture([220, 180, 120, 255]),
            solid_texture([128, 128, 255, 255]),
            solid_texture([255, 180, 0, 255]),
            solid_texture([30, 20, 10, 255]),
        ];
        let background = renderer
            .render(
                camera,
                &[],
                &[],
                [40, 80, 120, 255],
                RenderLimits {
                    width: 96,
                    height: 64,
                    max_triangles: 0,
                    max_texture_bytes: 0,
                    max_texture_pixels: 0,
                    far_distance: 64,
                },
            )
            .unwrap();
        assert!(
            background
                .chunks_exact(4)
                .all(|pixel| pixel == [40, 80, 120, 255]),
            "empty scene did not preserve its clear color"
        );
        let oriented = renderer
            .render(
                camera,
                &[renderable(Material::BlinnPhong(BlinnPhongMaterial {
                    diffuse: TextureSlot {
                        texture: Some(0),
                        ..Default::default()
                    },
                    fullbright: true,
                    ..Default::default()
                }))],
                &[Texture {
                    width: 2,
                    height: 2,
                    rgba: vec![
                        255, 0, 0, 255, 255, 0, 0, 255, 0, 0, 255, 255, 0, 0, 255, 255,
                    ],
                }],
                [0, 0, 0, 255],
                RenderLimits {
                    width: 96,
                    height: 64,
                    max_triangles: 2,
                    max_texture_bytes: 16,
                    max_texture_pixels: 4,
                    far_distance: 64,
                },
            )
            .unwrap();
        let top = &oriented[(20 * 96 + 48) * 4..][..4];
        let bottom = &oriented[(44 * 96 + 48) * 4..][..4];
        assert!(
            top[0] > top[2],
            "legacy texture top row is vertically flipped"
        );
        assert!(
            bottom[2] > bottom[0],
            "legacy texture bottom row is vertically flipped"
        );
        for renderable in frames {
            let rgba = renderer
                .render(
                    camera,
                    &[renderable],
                    &textures,
                    [0, 0, 0, 255],
                    RenderLimits {
                        width: 96,
                        height: 64,
                        max_triangles: 2,
                        max_texture_bytes: 64,
                        max_texture_pixels: 16,
                        far_distance: 64,
                    },
                )
                .unwrap();
            assert_eq!(rgba.len(), 96 * 64 * 4);
            assert!(
                rgba.chunks_exact(4).any(|pixel| pixel[..3] != [0, 0, 0]),
                "captured only black pixels; channel maxima are {:?}",
                (0..4)
                    .map(|channel| rgba.chunks_exact(4).map(|pixel| pixel[channel]).max())
                    .collect::<Vec<_>>()
            );
        }

        let limits = RenderLimits {
            width: 96,
            height: 64,
            max_triangles: 2,
            max_texture_bytes: 64,
            max_texture_pixels: 16,
            far_distance: 64,
        };
        renderer
            .load_scene(
                camera,
                &[renderable(Material::BlinnPhong(BlinnPhongMaterial {
                    diffuse_color_srgb: [30, 220, 60, 255],
                    fullbright: true,
                    ..Default::default()
                }))],
                &textures,
                limits,
            )
            .unwrap();
        for _ in 0..2 {
            let frame = renderer
                .render_view(camera, [0, 0, 0, 255], limits)
                .unwrap();
            assert_eq!(frame.rgba.len(), 96 * 64 * 4);
            assert_eq!(frame.timings.command_copy, Duration::ZERO);
            assert_eq!(frame.timings.texture_cache_sync, Duration::ZERO);
            assert_eq!(frame.timings.mesh_cache_sync, Duration::ZERO);
        }
    }

    fn texture_slot(texture: usize, rotation_radians: f32) -> TextureSlot {
        TextureSlot {
            texture: Some(texture),
            transform: UvTransform {
                scale: [1.5, 0.75],
                offset: [0.1, -0.1],
                rotation_radians,
            },
        }
    }

    fn solid_texture(pixel: [u8; 4]) -> Texture {
        Texture {
            width: 2,
            height: 2,
            rgba: pixel.repeat(4),
        }
    }

    fn renderable(material: Material) -> Renderable {
        let vertex = |position, tex_coord| Vertex {
            position,
            normal: [-1.0, 0.0, 0.0],
            tex_coord,
            color_srgb: [255; 4],
        };
        Renderable {
            mesh: SceneMesh {
                vertices: vec![
                    vertex([3.0, -0.8, -0.8], [0.0, 0.0]),
                    vertex([3.0, 0.8, -0.8], [1.0, 0.0]),
                    vertex([3.0, 0.8, 0.8], [1.0, 1.0]),
                    vertex([3.0, -0.8, 0.8], [0.0, 1.0]),
                ],
                indices: vec![0, 1, 2, 0, 2, 3],
            },
            material,
        }
    }
}
