use super::entities::*;
use super::state::*;
use crate::core::camera::CameraFactory;
use crate::core::film::FilmFactory;
use crate::core::filter::FilterFactory;
use crate::core::image::{io::ImageIO, Image};
use crate::core::material::MaterialFactory;
use crate::core::primitive::{CreateGeometricPrimitive, CreateSimplePrimitive};
use crate::core::sampler::SamplerFactory;
use crate::core::shape::{ShapeFactory, ShapeWithContext};
use crate::core::texture::{FloatTexture, SpectrumTexture};
use crate::utils::parallel::{run_async, AsyncJob};
use crate::utils::parameters::{NamedTextures, ParameterDictionary, TextureParameterDictionary};
use crate::utils::resolve_filename;
use crate::{Arena, ArenaUpload, FileLoc, Upload};
use anyhow::{anyhow, Result};
use parking_lot::Mutex;
use shared::core::camera::{Camera, CameraTransform};
use shared::core::color::LINEAR;
use shared::core::film::Film;
use shared::core::filter::Filter;
use shared::core::light::Light;
use shared::core::material::Material;
use shared::core::medium::{Medium, MediumInterface};
use shared::core::primitive::{AnimatedPrimitive, GeometricPrimitive, Primitive, SimplePrimitive};
use shared::core::sampler::Sampler;
use shared::core::shape::Shape;
use shared::core::texture::SpectrumType;
use shared::spectra::RGBColorSpace;
use shared::utils::Ptr;
use std::collections::HashMap;
use std::sync::Arc;

pub struct SceneLookup<'a> {
    pub textures: &'a NamedTextures,
    pub media: &'a HashMap<String, Arc<Medium>>,
    pub named_materials: &'a HashMap<String, Material>,
    pub materials: &'a Vec<Material>,
    pub shape_lights: &'a HashMap<usize, Vec<Light>>,
}

impl<'a> SceneLookup<'a> {
    pub fn find_medium(&self, name: &str, loc: &FileLoc) -> Option<Arc<Medium>> {
        if name.is_empty() {
            return None;
        }
        self.media.get(name).cloned().or_else(|| {
            log::error!("{}: medium '{}' not defined", loc, name);
            None
        })
    }

    pub fn resolve_material(&self, mat_ref: &MaterialRef, loc: &FileLoc) -> Option<Material> {
        match mat_ref {
            MaterialRef::Name(name) => match self.named_materials.get(name) {
                Some(m) => Some(*m),
                None => {
                    log::error!("{}: named material '{}' not found", loc, name);
                    None
                }
            },
            MaterialRef::Index(idx) => {
                if *idx < self.materials.len() {
                    Some(self.materials[*idx])
                } else {
                    log::error!("{}: material index {} out of bounds", loc, idx);
                    None
                }
            }
            MaterialRef::None => None,
        }
    }
}

pub struct BasicScene {
    pub integrator: Mutex<Option<SceneEntity>>,
    pub accelerator: Mutex<Option<SceneEntity>>,
    pub film_colorspace: Mutex<Option<Arc<RGBColorSpace>>>,

    pub shapes: Mutex<Vec<ShapeSceneEntity>>,
    pub animated_shapes: Mutex<Vec<AnimatedShapeSceneEntity>>,

    pub instances: Mutex<Vec<InstanceSceneEntity>>,
    pub instance_definitions: Mutex<HashMap<String, Arc<InstanceDefinitionSceneEntity>>>,

    pub media_state: Mutex<MediaState>,
    pub material_state: Mutex<MaterialState>,
    pub light_state: Mutex<LightState>,
    pub texture_state: Mutex<TextureState>,

    pub camera_state: Mutex<SingletonState<Camera>>,
    pub sampler_state: Mutex<SingletonState<Sampler>>,
    pub film_state: Mutex<SingletonState<Film>>,
}

impl BasicScene {
    pub fn new() -> Self {
        Self {
            integrator: Mutex::new(None),
            accelerator: Mutex::new(None),
            film_colorspace: Mutex::new(None),
            shapes: Mutex::new(Vec::new()),
            animated_shapes: Mutex::new(Vec::new()),
            instances: Mutex::new(Vec::new()),
            instance_definitions: Mutex::new(HashMap::new()),
            media_state: Mutex::new(MediaState::default()),
            material_state: Mutex::new(MaterialState::default()),
            light_state: Mutex::new(LightState::default()),
            texture_state: Mutex::new(TextureState::default()),
            camera_state: Mutex::new(SingletonState::default()),
            sampler_state: Mutex::new(SingletonState::default()),
            film_state: Mutex::new(SingletonState::default()),
        }
    }

    pub fn set_options(
        self: &Arc<Self>,
        filter: SceneEntity,
        film: SceneEntity,
        camera: CameraSceneEntity,
        sampler: SceneEntity,
        integ: SceneEntity,
        accel: SceneEntity,
        arena: &Arena,
    ) -> Result<()> {
        *self.integrator.lock() = Some(integ);
        *self.accelerator.lock() = Some(accel);

        if let Some(cs) = film.parameters.color_space.as_ref() {
            *self.film_colorspace.lock() = Some(Arc::clone(cs));
        }

        let filter = Filter::create(&filter.name, &filter.parameters, &filter.loc, &arena)
            .map_err(|e| anyhow!("Failed to create filter: {}", e))?;
        let shutter_close = camera.base.parameters.get_one_float("shutterclose", 1.)?;
        let shutter_open = camera.base.parameters.get_one_float("shutteropen", 0.)?;
        let exposure_time = shutter_close - shutter_open;

        let film_instance = Arc::new(
            Film::create(
                &film.name,
                &film.parameters,
                exposure_time,
                filter,
                Some(camera.camera_transform.clone()),
                &film.loc,
                &arena,
            )
            .map_err(|e| anyhow!("Failed to create film: {}", e))?,
        );

        *self.film_state.lock() = SingletonState {
            result: Some(Arc::clone(&film_instance)),
            job: None,
        };

        let arena_sampler = Arc::clone(&arena);
        let sampler_film = Arc::clone(&film_instance);
        let sampler_job = run_async(move || {
            let res = sampler_film.as_ref().base().full_resolution;
            Sampler::create(
                &sampler.name,
                &sampler.parameters,
                res,
                &sampler.loc,
                &arena_sampler,
            )
            .map_err(|e| anyhow!("Failed to create sampler: {}", e))
        });
        self.sampler_state.lock().job = Some(sampler_job);

        let arena_camera = Arc::clone(&arena);
        let camera_film = Arc::clone(&film_instance);
        let scene_ptr = Arc::clone(self);
        let camera_job = run_async(move || {
            let medium = scene_ptr.get_medium(&camera.medium, &camera.base.loc);
            Camera::create(
                &camera.base.name,
                &camera.base.parameters,
                &camera.camera_transform,
                medium,
                camera_film,
                &camera.base.loc,
                &arena_camera,
            )
            .map_err(|e| anyhow!("Failed to create camera: {}", e))
        });
        self.camera_state.lock().job = Some(camera_job);
        Ok(())
    }

    pub fn add_named_material(&self, name: &str, material: SceneEntity) {
        let mut state = self.material_state.lock();
        self.start_loading_normal_maps(&mut state, &material.parameters);
        state.named_materials.push((name.to_string(), material));
    }

    pub fn add_material(&self, material: SceneEntity) -> usize {
        let mut state = self.material_state.lock();
        self.start_loading_normal_maps(&mut state, &material.parameters);
        state.materials.push(material);
        state.materials.len() - 1
    }

    fn add_texture_generic<T, F>(
        &self,
        name: String,
        texture: TextureSceneEntity,
        state: &mut TextureState,
        get_serial: impl FnOnce(&mut TextureState) -> &mut Vec<(String, TextureSceneEntity)>,
        get_jobs: impl FnOnce(&mut TextureState) -> &mut HashMap<String, AsyncJob<Arc<T>>>,
        create_fn: F,
    ) -> Result<()>
    where
        T: Send + Sync + 'static,
        F: FnOnce(TextureSceneEntity) -> T + Send + 'static,
    {
        if texture.render_from_object.is_animated() {
            log::info!(
                "{}: Animated world to texture not supported, using start",
                texture.base.loc
            );
        }

        if texture.base.name != "imagemap" && texture.base.name != "ptex" {
            get_serial(state).push((name, texture));
            return Ok(());
        }

        let filename = resolve_filename(&texture.base.parameters.get_one_string("filename", "")?);
        if !self.validate_texture_file(&filename, &texture.base.loc, &mut state.n_missing_textures)
        {
            return Ok(());
        }

        if state.loading_texture_filenames.contains(&filename) {
            get_serial(state).push((name, texture));
            return Ok(());
        }

        state.loading_texture_filenames.insert(filename);
        let job = run_async(move || Arc::new(create_fn(texture)));
        get_jobs(state).insert(name, job);
        Ok(())
    }

    fn validate_texture_file(&self, filename: &str, loc: &FileLoc, n_missing: &mut usize) -> bool {
        if filename.is_empty() {
            eprintln!(
                "[{:?}] \"string filename\" not provided for image texture.",
                loc
            );
            *n_missing += 1;
            return false;
        }
        if !std::path::Path::new(filename).exists() {
            eprintln!("[{:?}] {}: file not found.", loc, filename);
            *n_missing += 1;
            return false;
        }
        true
    }

    pub fn add_float_texture(
        &self,
        name: String,
        texture: TextureSceneEntity,
        arena: Arc<Arena>,
    ) -> Result<()> {
        let mut state = self.texture_state.lock();
        self.add_texture_generic(
            name,
            texture,
            &mut state,
            |s| &mut s.serial_float_textures,
            |s| &mut s.float_texture_jobs,
            move |tex| {
                let render_from_texture = tex.render_from_object.start_transform;
                let tex_dict = TextureParameterDictionary::new(tex.base.parameters.into(), None);
                FloatTexture::create(
                    &tex.base.name,
                    render_from_texture,
                    tex_dict,
                    tex.base.loc,
                    &arena,
                )
                .expect("Could not create Float texture")
            },
        )
    }

    pub fn add_spectrum_texture(
        &self,
        name: String,
        texture: TextureSceneEntity,
        arena: Arc<Arena>,
    ) -> Result<()> {
        let mut state = self.texture_state.lock();
        self.add_texture_generic(
            name,
            texture,
            &mut state,
            |s| &mut s.serial_spectrum_textures,
            |s| &mut s.spectrum_texture_jobs,
            move |tex| {
                let render_from_texture = tex.render_from_object.start_transform;
                let tex_dict = TextureParameterDictionary::new(tex.base.parameters.into(), None);
                SpectrumTexture::create(
                    &tex.base.name,
                    render_from_texture,
                    tex_dict,
                    SpectrumType::Albedo,
                    tex.base.loc,
                    &arena,
                )
                .expect("Could not create spectrum texture")
            },
        )
    }

    pub fn add_area_light(&self, light: SceneEntity) -> usize {
        let mut state = self.light_state.lock();
        state.area_lights.push(light);
        state.area_lights.len() - 1
    }

    pub fn add_light(&self, light: LightSceneEntity) {
        self.light_state.lock().lights.push(light);
    }

    pub fn add_shape(&self, shape: ShapeSceneEntity) {
        self.shapes.lock().push(shape);
    }

    pub fn add_shapes(&self, new_shapes: Vec<ShapeSceneEntity>) {
        self.shapes.lock().extend(new_shapes);
    }

    pub fn add_animated_shapes(&self, new_shapes: Vec<AnimatedShapeSceneEntity>) {
        self.animated_shapes.lock().extend(new_shapes);
    }

    pub fn add_instance_definition(&self, instance: InstanceDefinitionSceneEntity) {
        let name = instance.name.clone();
        self.instance_definitions
            .lock()
            .insert(name, Arc::new(instance));
    }

    pub fn add_instance_uses(&self, uses: Vec<InstanceSceneEntity>) {
        self.instances.lock().extend(uses);
    }

    pub fn create_textures(&self, arena: &mut Arena) -> NamedTextures {
        let mut state = self.texture_state.lock();

        let mut float_textures: HashMap<String, Arc<FloatTexture>> = HashMap::new();
        let mut spectrum_textures: HashMap<String, Arc<SpectrumTexture>> = HashMap::new();

        // Collect async jobs
        for (name, job) in state.float_texture_jobs.drain() {
            float_textures.insert(name, job.wait());
        }

        for (name, job) in state.spectrum_texture_jobs.drain() {
            spectrum_textures.insert(name, job.wait());
        }

        // Create serial textures (need access to loaded textures, using shared memory)
        let mut named = NamedTextures {
            float_textures: Arc::new(float_textures.clone()),
            albedo_spectrum_textures: Arc::new(spectrum_textures.clone()),
            illuminant_spectrum_textures: Arc::new(spectrum_textures.clone()),
            unbounded_spectrum_textures: Arc::new(spectrum_textures.clone()),
        };

        for (name, entity) in state.serial_float_textures.drain(..) {
            let render_from_texture = entity.render_from_object.start_transform;
            let tex_dict =
                TextureParameterDictionary::new(entity.base.parameters.into(), Some(&named));
            let tex = FloatTexture::create(
                &entity.base.name,
                render_from_texture,
                tex_dict,
                entity.base.loc,
                arena,
            )
            .expect("Could not create float texture");
            Arc::make_mut(&mut named.float_textures).insert(name, Arc::new(tex));
        }

        for (name, entity) in state.serial_spectrum_textures.drain(..) {
            let render_from_texture = entity.render_from_object.start_transform;
            let tex_dict =
                TextureParameterDictionary::new(entity.base.parameters.into(), Some(&named));
            let tex = SpectrumTexture::create(
                &entity.base.name,
                render_from_texture,
                tex_dict,
                SpectrumType::Albedo,
                entity.base.loc,
                arena,
            )
            .expect("Could not create spectrum texture");
            Arc::make_mut(&mut named.albedo_spectrum_textures).insert(name, Arc::new(tex));
        }
        named
    }

    // Assuming that we can carry on if a material is missing.
    // This might be a bad idea, but testing it for now (2026/02/19)
    pub fn create_materials(
        &self,
        textures: &NamedTextures,
        arena: &mut Arena,
    ) -> Result<(HashMap<String, Material>, Vec<Material>)> {
        let mut state = self.material_state.lock();

        let finished: Vec<_> = state.normal_map_jobs.drain().collect();
        for (filename, job) in finished {
            match std::panic::catch_unwind(|| job.wait()) {
                Ok(img) => {
                    state.normal_maps.insert(filename, img);
                }
                Err(_) => {
                    log::error!("Failed to load normal map: {}", filename);
                }
            }
        }

        let mut named_materials: HashMap<String, Material> = HashMap::new();

        for (name, entity) in &state.named_materials {
            if named_materials.contains_key(name) {
                log::error!(
                    "{}: trying to redefine named material '{}'.",
                    entity.loc,
                    name
                );
                continue;
            }

            let mat_type = entity.parameters.get_one_string("type", "")?;
            if mat_type.is_empty() {
                log::error!("{}: missing material type", entity.loc);
                continue;
            }

            let normal_map = self.get_normal_map(&state, &entity.parameters)?;
            let tex_dict = TextureParameterDictionary::new(
                Arc::new(entity.parameters.clone()),
                Some(textures),
            );

            match Material::create(
                &mat_type,
                &tex_dict,
                normal_map,
                &named_materials,
                entity.loc.clone(),
                arena,
            ) {
                Ok(mat) => {
                    named_materials.insert(name.clone(), mat);
                }
                Err(e) => {
                    log::error!(
                        "{}: Failed to create material '{}': {}",
                        entity.loc,
                        name,
                        e
                    );
                }
            }
        }

        let materials: Vec<Material> = state
            .materials
            .iter()
            .filter_map(|entity| {
                let result: Result<Material> = (|| {
                    let normal_map = self.get_normal_map(&state, &entity.parameters)?;
                    let tex_dict = TextureParameterDictionary::new(
                        entity.parameters.clone().into(),
                        Some(textures),
                    );

                    Material::create(
                        &entity.name,
                        &tex_dict,
                        normal_map,
                        &named_materials,
                        entity.loc.clone(),
                        arena,
                    )
                })();

                match result {
                    Ok(mat) => Some(mat),
                    Err(e) => {
                        log::error!("{}: Failed to create material: {}", entity.loc, e);
                        None
                    }
                }
            })
            .collect();

        Ok((named_materials, materials))
    }

    pub fn create_lights(
        &self,
        camera_transform: &CameraTransform,
        arena: &mut Arena,
    ) -> Vec<Light> {
        let state = self.light_state.lock();

        state
            .lights
            .iter()
            .filter_map(|entity| {
                let render_from_light = entity.transformed_base.render_from_object.start_transform;

                let medium = self
                    .get_medium(&entity.medium, &entity.transformed_base.base.loc)
                    .map(|m| *m);

                match crate::core::light::create_light(
                    &entity.transformed_base.base.name,
                    render_from_light,
                    medium,
                    &entity.transformed_base.base.parameters,
                    &entity.transformed_base.base.loc,
                    camera_transform.clone(),
                    arena,
                ) {
                    Ok(light) => Some(light),
                    Err(e) => {
                        log::error!(
                            "{}: failed to create light: {}",
                            entity.transformed_base.base.loc,
                            e
                        );
                        None
                    }
                }
            })
            .collect()
    }

    /// Create area lights for shapes that reference one. Produces a map from
    /// shape index to a vec of lights.
    /// Must be called after shapes are loaded but before upload_shapes.
    pub fn create_area_lights(
        &self,
        loaded_shapes: &[Vec<Shape>],
        shape_entities: &[ShapeSceneEntity],
        textures: &NamedTextures,
        arena: &Arena,
    ) -> HashMap<usize, Vec<Light>> {
        let light_state = self.light_state.lock();
        let mut shape_lights: HashMap<usize, Vec<Light>> = HashMap::new();

        for (i, entity) in shape_entities.iter().enumerate() {
            let light_idx = match entity.light_index {
                Some(idx) => idx,
                None => continue,
            };

            let shapes = match loaded_shapes.get(i) {
                Some(s) if !s.is_empty() => s,
                _ => continue,
            };

            let al_entity = &light_state.area_lights[light_idx];

            let alpha_tex = Self::get_alpha_texture(
                &entity.base.parameters,
                &entity.base.loc,
                &textures.float_textures,
            );

            let default_alpha = Arc::new(FloatTexture::default());
            let alpha_ref = alpha_tex.as_ref().unwrap_or(&default_alpha);

            // Use the film colorspace as fallback for area light emission
            let film_cs = self.film_colorspace.lock();
            let colorspace_ref = al_entity
                .parameters
                .color_space
                .as_ref()
                .or(film_cs.as_ref());

            let render_from_light = *entity.render_from_object;

            let lights: Vec<Light> = shapes
                .iter()
                .filter_map(|shape| {
                    match crate::core::light::create_area_light(
                        render_from_light,
                        None,
                        &al_entity.parameters,
                        &al_entity.loc,
                        shape,
                        alpha_ref,
                        colorspace_ref.map(|cs| cs.as_ref()),
                        arena,
                    ) {
                        Ok(light) => Some(light),
                        Err(e) => {
                            log::error!("{}: failed to create area light: {}", al_entity.loc, e);
                            None
                        }
                    }
                })
                .collect();

            if !lights.is_empty() {
                shape_lights.insert(i, lights);
            }
        }

        shape_lights
    }

    pub fn create_aggregate(
        &self,
        textures: &NamedTextures,
        named_materials: &HashMap<String, Material>,
        materials: &[Material],
        arena: &mut Arena,
    ) -> (Vec<Primitive>, Vec<Arc<Light>>) {
        let entities = self.shapes.lock();
        let animated = self.animated_shapes.lock();
        let light_state = self.light_state.lock();
        let media = self.media_state.lock();
        let film_cs = self.film_colorspace.lock();

        let mut primitives = Vec::new();
        let mut area_lights = Vec::new();

        for entity in entities.iter() {
            Self::build_primitives_for_entity(
                entity,
                textures,
                named_materials,
                materials,
                &light_state,
                &media,
                film_cs.as_ref().map(|v| &**v),
                arena,
                &mut primitives,
                &mut area_lights,
            );
        }

        for entity in animated.iter() {
            Self::build_animated_primitives_for_entity(
                entity,
                textures,
                named_materials,
                materials,
                &light_state,
                &media,
                film_cs.as_ref().map(|v| &**v),
                arena,
                &mut primitives,
                &mut area_lights,
            );
        }

        (primitives, area_lights)
    }

    fn build_primitives_inner(
        shapes: Vec<Ptr<Shape>>,
        mtl: Material,
        alpha_tex: &Option<Arc<FloatTexture>>,
        mi: MediumInterface,
        al_params: Option<&AreaLightEntity>,
        render_from_light: Transform,
        film_cs: Option<&RGBColorSpace>,
        arena: &mut Arena,
        area_lights: &mut Vec<Arc<Light>>,
    ) -> Vec<(Ptr<Shape>, Ptr<Light>, Ptr<GPUFloatTexture>)> {
        shapes
            .into_iter()
            .map(|shape| {
                let area_light_ptr = al_params
                    .and_then(|al_entity| {
                        let cs = al_entity.parameters.color_space.as_deref().or(film_cs);
                        let default_alpha = Arc::new(FloatTexture::default());
                        let alpha_ref = alpha_tex.as_ref().unwrap_or(&default_alpha);
                        crate::core::light::create_area_light(
                            render_from_light,
                            None,
                            &al_entity.parameters,
                            &al_entity.loc,
                            &shape,
                            alpha_ref,
                            cs,
                            arena,
                        )
                        .ok()
                    })
                    .unwrap_or(Ptr::null());

                if !area_light_ptr.is_null() {
                    area_lights.push(Arc::new(unsafe { &*area_light_ptr.as_raw() }.clone()));
                }

                let alpha_ptr = alpha_tex
                    .as_ref()
                    .map(|t| arena.upload(t.as_ref()))
                    .unwrap_or(Ptr::null());

                (shape, area_light_ptr, alpha_ptr)
            })
            .collect()
    }

    fn build_primitives_for_entity(
        entity: &ShapeSceneEntity,
        textures: &NamedTextures,
        named_materials: &HashMap<String, Material>,
        materials: &[Material],
        light_state: &LightState,
        media: &MediaState,
        film_cs: Option<&RGBColorSpace>,
        arena: &mut Arena,
        primitives: &mut Vec<Primitive>,
        area_lights: &mut Vec<Arc<Light>>,
    ) {
        let shapes = Shape::create(
            &entity.base.name,
            *entity.render_from_object.as_ref(),
            *entity.object_from_render.as_ref(),
            entity.reverse_orientation,
            entity.base.parameters.clone(),
            &textures.float_textures,
            entity.base.loc.clone(),
            arena,
        )
        .unwrap_or_else(|e| {
            eprintln!("Shape '{}' failed: {}", entity.base.name, e);
            Vec::new()
        });

        let mtl = match &entity.material {
            MaterialRef::Name(name) => named_materials.get(name).copied(),
            MaterialRef::Index(idx) => materials.get(*idx).copied(),
            MaterialRef::None => None,
        }
        .unwrap_or_else(|| crate::core::material::default_diffuse_material(arena));

        let alpha_tex = Self::get_alpha_texture(
            &entity.base.parameters,
            &entity.base.loc,
            &textures.float_textures,
        );

        let mi = MediumInterface {
            inside: media
                .map
                .get(&entity.inside_medium)
                .map(|m| Ptr::from(m.as_ref()))
                .unwrap_or(Ptr::null()),
            outside: media
                .map
                .get(&entity.outside_medium)
                .map(|m| Ptr::from(m.as_ref()))
                .unwrap_or(Ptr::null()),
        };

        let al_params = entity.light_index.map(|idx| &light_state.area_lights[idx]);

        let built = Self::build_primitives_inner(
            shapes,
            mtl,
            &alpha_tex,
            mi.clone(),
            al_params,
            *entity.render_from_object,
            film_cs,
            arena,
            area_lights,
        );

        for (shape, light_ptr, alpha_ptr) in built {
            let prim = if light_ptr.is_null() && !mi.is_medium_transition() && alpha_tex.is_none() {
                Primitive::Simple(SimplePrimitive::new(shape, Ptr::from(&mtl)))
            } else {
                Primitive::Geometric(GeometricPrimitive::new(
                    shape,
                    arena.alloc(mtl),
                    light_ptr,
                    mi.clone(),
                    alpha_ptr,
                ))
            };
            primitives.push(prim);
        }
    }

    fn build_animated_primitives_for_entity(
        entity: &AnimatedShapeSceneEntity,
        textures: &NamedTextures,
        named_materials: &HashMap<String, Material>,
        materials: &[Material],
        light_state: &LightState,
        media: &MediaState,
        film_cs: Option<&RGBColorSpace>,
        arena: &mut Arena,
        primitives: &mut Vec<Primitive>,
        area_lights: &mut Vec<Arc<Light>>,
    ) {
        let shapes = Shape::create(
            &entity.transformed_base.base.name,
            entity.transformed_base.render_from_object.start_transform,
            entity
                .transformed_base
                .render_from_object
                .start_transform
                .inverse(),
            entity.reverse_orientation,
            entity.transformed_base.base.parameters.clone(),
            &textures.float_textures,
            entity.transformed_base.base.loc.clone(),
            arena,
        )
        .unwrap_or_else(|e| {
            eprintln!(
                "Animated shape '{}' failed: {}",
                entity.transformed_base.base.name, e
            );
            Vec::new()
        });

        let mtl = match &entity.material {
            MaterialRef::Name(name) => named_materials.get(name).copied(),
            MaterialRef::Index(idx) => materials.get(*idx).copied(),
            MaterialRef::None => None,
        }
        .unwrap_or_else(|| crate::core::material::default_diffuse_material(arena));

        let alpha_tex = Self::get_alpha_texture(
            &entity.transformed_base.base.parameters,
            &entity.transformed_base.base.loc,
            &textures.float_textures,
        );

        let mi = MediumInterface {
            inside: media
                .map
                .get(&entity.inside_medium)
                .map(|m| Ptr::from(m.as_ref()))
                .unwrap_or(Ptr::null()),
            outside: media
                .map
                .get(&entity.outside_medium)
                .map(|m| Ptr::from(m.as_ref()))
                .unwrap_or(Ptr::null()),
        };

        let al_params = entity.light_index.map(|idx| &light_state.area_lights[idx]);

        let built = Self::build_primitives_inner(
            shapes,
            mtl,
            &alpha_tex,
            mi.clone(),
            al_params,
            entity.transformed_base.render_from_object.start_transform,
            film_cs,
            arena,
            area_lights,
        );

        for (shape, light_ptr, alpha_ptr) in built {
            let base_prim =
                if light_ptr.is_null() && !mi.is_medium_transition() && alpha_tex.is_none() {
                    Primitive::Simple(SimplePrimitive::new(shape, Ptr::from(&mtl)))
                } else {
                    Primitive::Geometric(GeometricPrimitive::new(
                        shape,
                        arena.alloc(mtl),
                        light_ptr,
                        mi.clone(),
                        alpha_ptr,
                    ))
                };
            primitives.push(Primitive::Animated(AnimatedPrimitive {
                primitive: arena.alloc(base_prim),
                render_from_primitive: arena.alloc(entity.transformed_base.render_from_object),
            }));
        }
    }

    fn upload_shapes(
        &self,
        arena: &Arena,
        entities: &[ShapeSceneEntity],
        loaded: Vec<ShapeWithContext>,
        lookup: &SceneLookup,
    ) -> Vec<Primitive> {
        let mut primitives = Vec::new();
        let mut count = 0;

        for shape_ctx in loaded {
            count += 1;
            if count % 500_000 == 0 {
                eprintln!("  processed {} primitives", count);
            }
            let entity = &entities[shape_ctx.entity_index];

            let alpha_tex = Self::get_alpha_texture(
                &entity.base.parameters,
                &entity.base.loc,
                &lookup.textures.float_textures,
            );

            let mtl = lookup
                .resolve_material(&entity.material, &entity.base.loc)
                .unwrap_or_else(|| crate::core::material::default_diffuse_material(arena));

            let mi = MediumInterface {
                inside: lookup
                    .find_medium(&entity.inside_medium, &entity.base.loc)
                    .map(|m| Ptr::from(m.as_ref()))
                    .unwrap_or(Ptr::null()),
                outside: lookup
                    .find_medium(&entity.outside_medium, &entity.base.loc)
                    .map(|m| Ptr::from(m.as_ref()))
                    .unwrap_or(Ptr::null()),
            };

            let shape_lights_opt = lookup
                .shape_lights
                .get(&shape_ctx.entity_index)
                .and_then(|lights| lights.get(shape_ctx.shape_index));

            let light_ptr = arena.alloc_opt(shape_lights_opt);
            let shape_ptr = shape_ctx.shape;
            let prim = if area_light.is_null() && !mi.is_medium_transition() && alpha_tex.is_none()
            {
                Primitive::Simple(SimplePrimitive::new(shape_ptr, Ptr::from(&mtl)))
            } else {
                Primitive::Geometric(GeometricPrimitive::new(
                    shape_ptr,
                    arena.alloc(mtl),
                    area_light,
                    mi.clone(),
                    arena.upload(alpha_tex),
                ))
            };

            primitives.push(prim);
        }

        primitives
    }

    fn upload_animated_shapes(
        &self,
        _arena: &mut Arena,
        _entities: &[AnimatedShapeSceneEntity],
        _loaded: Vec<Ptr<Shape>>,
        _lookup: &SceneLookup,
    ) -> Vec<Primitive> {
        // TODO: implement animated shape upload
        Vec::new()
    }

    // Getters

    pub fn get_camera(&self) -> Result<Arc<Camera>> {
        self.get_singleton(&self.camera_state, "Camera")
    }

    pub fn get_sampler(&self) -> Result<Arc<Sampler>> {
        self.get_singleton(&self.sampler_state, "Sampler")
    }

    pub fn get_film(&self) -> Result<Arc<Film>> {
        self.get_singleton(&self.film_state, "Film")
    }

    // Helpers

    fn get_singleton<T: Send + 'static>(
        &self,
        state: &Mutex<SingletonState<T>>,
        name: &str,
    ) -> Result<Arc<T>> {
        let mut guard = state.lock();

        if let Some(ref res) = guard.result {
            return Ok(res.clone());
        }

        if let Some(job) = guard.job.take() {
            let val = job.wait()?;
            let res = Arc::new(val);
            guard.result = Some(res.clone());
            return Ok(res);
        }

        Err(anyhow!("{} requested but not initialized!", name))
    }

    fn start_loading_normal_maps(
        &self,
        state: &mut MaterialState,
        params: &ParameterDictionary,
    ) -> Result<()> {
        let filename = resolve_filename(&params.get_one_string("normalmap", "")?);
        if filename.is_empty() {
            return Ok(());
        }

        if state.normal_map_jobs.contains_key(&filename)
            || state.normal_maps.contains_key(&filename)
        {
            return Ok(());
        }

        let filename_clone = filename.clone();
        let job = run_async(move || {
            let path = std::path::Path::new(&filename_clone);
            let immeta = Image::read(path, Some(LINEAR)).expect(&format!(
                "{}: normal map must contain R, G, B channels",
                filename_clone
            ));

            let rgb_desc = immeta
                .image
                .get_channel_desc(&["R", "G", "B"])
                .expect(&format!(
                    "{}: normal map must contain R, G, B channels",
                    filename_clone
                ));

            Arc::new(immeta.image.select_channels(&rgb_desc))
        });

        state.normal_map_jobs.insert(filename, job);
        Ok(())
    }

    fn get_normal_map(
        &self,
        state: &MaterialState,
        params: &ParameterDictionary,
    ) -> Result<Option<Arc<Image>>> {
        let filename = resolve_filename(&params.get_one_string("normalmap", "")?);
        if filename.is_empty() {
            return Ok(None);
        }
        Ok(state.normal_maps.get(&filename).cloned())
    }

    fn get_alpha_texture(
        params: &ParameterDictionary,
        loc: &FileLoc,
        textures: &HashMap<String, Arc<FloatTexture>>,
    ) -> Option<Arc<FloatTexture>> {
        let name = params.get_texture("alpha");
        if name.is_empty() {
            return None;
        }
        match textures.get(&name) {
            Some(tex) => Some(tex.clone()),
            None => panic!("{:?}: Alpha texture '{}' not found", loc, name),
        }
    }

    pub fn get_medium(&self, name: &str, loc: &FileLoc) -> Option<Arc<Medium>> {
        if name.is_empty() {
            return None;
        }

        let mut state = self.media_state.lock();

        if let Some(medium) = state.map.get(name) {
            return Some(Arc::clone(medium));
        }

        if let Some(job) = state.jobs.remove(name) {
            let job: AsyncJob<Medium> = job;
            let result: Medium = job.wait();
            let medium: Arc<Medium> = Arc::new(result);
            state.map.insert(name.to_string(), medium.clone());
            return Some(medium);
        }

        log::error!("{}: Medium \"{}\" is not defined.", loc, name);
        None
    }
}
