use crate::core::filter::FilterFactory;
use crate::core::image::{Image, io::ImageIO};
use crate::core::material::MaterialFactory;
use crate::utils::parallel::{AsyncJob, run_async};
use crate::utils::parameters::{
    NamedTextures, ParameterDictionary, ParsedParameterVector, TextureParameterDictionary,
};
use crate::utils::parser::ParserTarget;
use crate::utils::{Arena, Upload};
use crate::utils::{normalize_utf8, resolve_filename};
use image_rs::Primitive;
use parking_lot::Mutex;
use shared::Float;
use shared::core::camera::{Camera, CameraTransform};
use shared::core::color::ColorEncoding;
use shared::textures::*;
// use shared::core::color::LINEAR;
use crate::core::texture::{FloatTexture, SpectrumTexture};
use crate::utils::error::FileLoc;
use shared::core::film::Film;
use shared::core::filter::Filter;
use shared::core::geometry::Vector3f;
use shared::core::light::Light;
use shared::core::material::Material;
use shared::core::medium::{Medium, MediumInterface};
use shared::core::options::RenderingCoordinateSystem;
use shared::core::primitive::{GeometricPrimitive, PrimitiveTrait, SimplePrimitive};
use shared::core::sampler::Sampler;
use shared::core::shape::Shape;
use shared::core::texture::SpectrumType;
use shared::spectra::RGBColorSpace;
use shared::utils::transform::{AnimatedTransform, Transform, look_at};
use std::collections::{HashMap, HashSet};
use std::ops::{Index as IndexTrait, IndexMut as IndexMutTrait};
use std::sync::Arc;
// use std::sync::atomic::{AtomicI32, Ordering};

#[derive(Clone, Debug)]
pub enum MaterialRef {
    Index(usize),
    Name(String),
    None,
}

#[derive(Clone, Default, Debug)]
pub struct SceneEntity {
    pub name: String,
    pub loc: FileLoc,
    pub parameters: ParameterDictionary,
}

#[derive(Clone, Debug)]
pub struct TransformedSceneEntity {
    pub base: SceneEntity,
    pub render_from_object: AnimatedTransform,
}

pub type MediumSceneEntity = TransformedSceneEntity;
pub type TextureSceneEntity = TransformedSceneEntity;

#[derive(Clone, Debug)]
pub struct CameraSceneEntity {
    pub base: SceneEntity,
    pub camera_transform: CameraTransform,
    pub medium: String,
}

#[derive(Clone, Debug)]
pub struct ShapeSceneEntity {
    pub base: SceneEntity,
    pub render_from_object: Arc<Transform>,
    pub object_from_render: Arc<Transform>,
    pub reverse_orientation: bool,
    pub material: MaterialRef,
    pub light_index: Option<usize>,

    pub inside_medium: String,
    pub outside_medium: String,
}

#[derive(Clone, Debug)]
pub struct AnimatedShapeSceneEntity {
    pub transformed_base: TransformedSceneEntity,

    pub identity: Arc<Transform>,
    pub reverse_orientation: bool,
    pub material: MaterialRef,
    pub light_index: Option<usize>,
    pub inside_medium: String,
    pub outside_medium: String,
}

#[derive(Clone, Debug)]
pub struct InstanceDefinitionSceneEntity {
    pub name: String,
    pub loc: FileLoc,
    pub shapes: Vec<ShapeSceneEntity>,
    pub animated_shapes: Vec<AnimatedShapeSceneEntity>,
}

#[derive(Clone, Debug)]
pub struct LightSceneEntity {
    pub transformed_base: TransformedSceneEntity,
    pub medium: String,
}

#[derive(Clone, Debug)]
pub enum InstanceTransform {
    Animated(AnimatedTransform),
    Static(Arc<Transform>),
}

#[derive(Clone, Debug)]
pub struct InstanceSceneEntity {
    pub name: String,
    pub loc: FileLoc,
    pub transform: InstanceTransform,
}

#[derive(Default)]
pub struct TextureState {
    pub serial_float_textures: Vec<(String, TextureSceneEntity)>,
    pub serial_spectrum_textures: Vec<(String, TextureSceneEntity)>,
    pub async_spectrum_textures: Vec<(String, TextureSceneEntity)>,
    pub loading_texture_filenames: HashSet<String>,
    pub float_texture_jobs: HashMap<String, AsyncJob<Arc<FloatTexture>>>,
    pub spectrum_texture_jobs: HashMap<String, AsyncJob<Arc<SpectrumTexture>>>,
    pub n_missing_textures: i32,
}

#[derive(Default)]
pub struct MaterialState {
    pub named_materials: Vec<(String, SceneEntity)>,
    pub materials: Vec<SceneEntity>,
    pub normal_map_jobs: HashMap<String, AsyncJob<Arc<Image>>>,
    pub normal_maps: HashMap<String, Arc<Image>>,
}

#[derive(Default)]
pub struct LightState {
    pub light_jobs: Vec<AsyncJob<Light>>,
    pub area_lights: Vec<SceneEntity>,
}

#[derive(Default)]
pub struct MediaState {
    pub jobs: HashMap<String, AsyncJob<Medium>>,
    pub map: HashMap<String, Arc<Medium>>,
}

#[derive(Default)]
pub struct SingletonState<T> {
    pub result: Option<Arc<T>>,
    pub job: Option<AsyncJob<T>>,
}

pub struct BasicScene {
    pub integrator: Option<SceneEntity>,
    pub accelerator: Option<SceneEntity>,
    pub film_colorspace: 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>>,
}

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

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

    fn resolve_material(&self, name: &str, loc: &FileLoc) -> Material {
        if !name.is_empty() {
            *self.named_materials.get(name).expect("Material not found")
        } // else {
        //     self.materials[index]
        // }
    }
}

impl BasicScene {
    fn set_options(
        self: &Arc<Self>,
        filter: SceneEntity,
        film: SceneEntity,
        camera: CameraSceneEntity,
        sampler: SceneEntity,
        integ: SceneEntity,
        accel: SceneEntity,
    ) {
        *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 filt = Filter::create(&filter.name, filter.parameters, &filter.loc);
        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,
                filt.expect("Must have a filter"),
                Some(camera.camera_transform.clone()),
                &film.loc,
            )
            .expect("Must have a film"),
        );

        *self.film.lock() = Some(Arc::clone(&film_instance));
        let sampler_film = Arc::clone(&film_instance);
        let sampler_job = std::thread::spawn(move || {
            let res = sampler_film.as_ref().base().full_resolution;
            Sampler::create(&sampler.name, &sampler.parameters, res, &sampler.loc)
                .expect("Sampler was not correctly created")
        });
        *self.sampler_job.lock() = Some(sampler_job);

        let camera_film = Arc::clone(&film_instance);
        let scene_ptr = self.clone();
        let camera_job = std::thread::spawn(move || {
            let medium = scene_ptr.get_medium(&camera.medium, &camera.base.loc);
            Camera::create(
                &camera.base.name,
                &camera.base.parameters,
                medium.unwrap(),
                &camera.camera_transform,
                camera_film,
                &camera.base.loc,
            )
            .expect("Failed to create camera")
        });
        *self.camera_job.lock() = Some(camera_job);
    }

    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, name: &str, 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
    }

    pub fn add_float_texture(&self, name: &str, texture: &TextureSceneEntity) {
        if texture.render_from_object.is_animated() {
            log::info!(
                "{}: Animated world to texture not supported, using start",
                texture.base.loc
            );
        }

        let mut texture_state = self.texture_data.lock();
        if name != "imagemap" && name != "ptex" {
            texture_state
                .serial_float_textures
                .push((name.to_string(), texture.clone()));
            return;
        }

        let filename = resolve_filename(&texture.base.parameters.get_one_string("filename", ""));
        if filename.is_empty() {
            log::error!(
                "[{:?}] \"string filename\" not provided for image texture.",
                texture.base.loc
            );
            texture_state.n_missing_textures += 1;
            return;
        }
        if !std::path::Path::new(&filename).exists() {
            log::error!("[{:?}] {}: file not found.", texture.base.loc, filename);
            texture_state.n_missing_textures += 1;
            return;
        }

        if texture_state.loading_texture_filenames.contains(&filename) {
            texture_state
                .serial_float_textures
                .push((name.to_string(), texture.clone()));
            return;
        }

        texture_state
            .loading_texture_filenames
            .insert(filename.clone());

        let texture_clone = texture.clone();
        let name_clone = name.to_string();

        let job = run_async(move || {
            let render_from_texture = texture_clone.render_from_object.start_transform();
            let tex_dict =
                TextureParameterDictionary::new(&texture_clone.base.parameters.into(), None);

            FloatTexture::create(
                &texture_clone.name,
                render_from_texture,
                tex_dict,
                &texture_clone.base.loc,
            )
        });

        texture_state
            .float_texture_jobs
            .insert(name.to_string(), job);
    }

    pub fn add_spectrum_texture(&self, name: &str, texture: &TextureSceneEntity) {
        if texture.render_from_object.is_animated() {
            log::info!(
                "{}: Animated world to texture not supported, using start",
                texture.base.loc
            );
        }

        let mut texture_state = self.texture_data.lock();
        if name != "imagemap" && name != "ptex" {
            texture_state
                .serial_spectrum_textures
                .push((name.to_string(), texture.clone()));
            return;
        }

        let filename = resolve_filename(&texture.base.parameters.get_one_string("filename", ""));
        if filename.is_empty() {
            log::error!(
                "[{:?}] \"string filename\" not provided for image texture.",
                texture.base.loc
            );
            texture_state.n_missing_textures += 1;
            return;
        }

        if !std::path::Path::new(&filename).exists() {
            log::error!("[{:?}] {}: file not found.", texture.base.loc, filename);
            texture_state.n_missing_textures += 1;
            return;
        }

        if texture_state.loading_texture_filenames.contains(&filename) {
            texture_state
                .serial_float_textures
                .push((name.to_string(), texture.clone()));
            return;
        }

        texture_state
            .loading_texture_filenames
            .insert(filename.clone());

        let texture_clone = texture.clone();
        let name_clone = name.to_string();

        let job = run_async(move || {
            let render_from_texture = texture_clone.render_from_object.start_transform();
            let tex_dict =
                TextureParameterDictionary::new(&texture_clone.base.parameters.into(), None);

            SpectrumTexture::create(
                &texture_clone.name,
                render_from_texture,
                tex_dict,
                SpectrumType::Albedo,
                &texture_clone.base.loc,
            )
        });

        texture_state
            .float_texture_jobs
            .insert(name.to_string(), job);
    }

    pub fn add_light(&self, light: LightSceneEntity) {
        if light.transformed_base.render_from_object.is_animated() {
            log::warn!(
                "{}: Animated world to texture not supported, using start",
                light.transformed_base.base.loc
            );
        }

        let medium = self
            .get_medium(light.medium, &light.transformed_base.base.loc)
            .unwrap();
        let camera_transform = self.get_camera().camera_transform;

        let light_clone = light.clone();
        let mut light_state = self.light_state.lock().unwrap();
        let job = run_async(move || {
            let render_from_light = light_clone
                .transformed_base
                .render_from_object
                .start_transform;

            Light::create(
                &light_clone.transformed_base.base.name,
                &light_clone.transformed_base.base.parameters,
                render_from_light,
                camera_transform,
                medium,
                &light_clone.transformed_base.base.loc,
            )
        });
        light_state.light_jobs.insert(job);
    }

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

    pub fn add_shapes(&self, new_shapes: &mut Vec<ShapeSceneEntity>) {
        let mut shapes = self.shapes.lock().unwrap();
        shapes.append(new_shapes);
    }

    pub fn add_animated_shapes(&self, new_shapes: &mut Vec<ShapeSceneEntity>) -> usize {
        let mut shapes = self.animated_shapes.lock().unwrap();
        shapes.append(new_shapes);
    }

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

    pub fn add_instance_uses(&self, new_defs: &mut Vec<InstanceDefinitionSceneEntity>) {
        let mut defs = self.instances.lock().unwrap();
        defs.append(new_defs);
    }

    pub fn create_materials(
        &self,
        textures: &NamedTextures,
        arena: &mut Arena,
    ) -> (HashMap<String, Material>, Vec<Material>) {
        let mut state = self.material_state.lock().unwrap();
        log::info!(
            "Starting to consume {} normal map futures",
            state.normal_map_jobs.len()
        );

        let jobs: Vec<_> = state.normal_maps_jobs.drain().collect();
        for (filename, job) in jobs {
            if state.normal_map_jobs.contains_key(&filename) {
                panic!("Trying to redefine '{}' twice in jobs and maps", filename);
            }

            let image = job.wait();
            state.normal_map_jobs.insert(filename, image);
        }

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

        for (name, entity) in &state.named_materials {
            if named_materials_out.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!(
                    "{}: \"string type\" not provided in named material's parameters.",
                    entity.loc
                );
                continue;
            }

            let nm_filename = resolve_filename(&entity.parameters.get_one_string("normalmap", ""));
            let mut normal_map_img = None;

            if !nm_filename.is_empty() {
                if let Some(img) = state.normal_maps.get(&nm_filename) {
                    normal_map_img = Some(img.clone());
                } else {
                    panic!(
                        "Normal map '{}' was expected but not found in map.",
                        nm_filename
                    );
                }
            }

            let tex_dict = TextureParameterDictionary::new(&entity.parameters, textures);

            let mat = Material::create(
                name,
                &tex_dict,
                normal_map_img,
                &named_materials_out,
                &entity.loc,
                arena,
            );

            named_materials_out.insert(name.clone(), mat);
        }

        let mut materials_out: Vec<Material> = Vec::with_capacity(state.materials.len());
        for entity in &state.materials {
            let nm_filename = resolve_filename(&entity.parameters.get_one_string("normalmap", ""));
            let mut normal_map_img = None;

            if !nm_filename.is_empty() {
                if let Some(img) = state.normal_maps.get(&nm_filename) {
                    normal_map_img = Some(img.clone());
                } else {
                    panic!(
                        "Normal map '{}' was expected but not found in map.",
                        nm_filename
                    );
                }
            }

            let tex_dict = TextureParameterDictionary::new(&entity.parameters, textures);

            let mat = Material::create(
                entity.materials.name,
                &tex_dict,
                normal_map_img,
                &named_materials_out,
                &entity.loc,
                arena,
            );
        }
        (named_materials_out, materials_out)
    }

    pub fn create_aggregate(
        &self,
        arena: &mut Arena,
        textures: &NamedTextures,
        shape_lights: &HashMap<usize, Vec<Light>>,
    ) -> Arc<dyn PrimitiveTrait> {
        let shapes_guard = self.shapes.lock().unwrap();
        let animated_shapes_guard = self.animated_shapes.lock().unwrap();
        let media_guard = self.media_state.lock().unwrap();
        let mat_guard = self.material_state.lock().unwrap();

        let lookup = SceneLookup {
            textures: &textures.float_textures,
            media: &media_guard.map,
            named_materials: &mat_guard.named_materials.iter().cloned().collect(),
            materials: &mat_guard.materials,
            shape_lights,
        };

        let mut all_primitives = Vec::new();

        // Parallel CPU Load
        let loaded_static = self.load_shapes_parallel(&shapes_guard, &lookup);
        // Serial Arena Upload
        let static_prims = self.upload_shapes(arena, &shapes_guard, loaded_static, &lookup);
        all_primitives.extend(static_prims);

        // Parallel CPU Load
        let loaded_animated = self.load_animated_shapes_parallel(&animated_shapes_guard, &lookup);
        // Serial Arena Upload
        let anim_prims =
            self.upload_animated_shapes(arena, &animated_shapes_guard, loaded_animated, &lookup);
        all_primitives.extend(anim_prims);

        // (Similar pattern: Lock definitions, load parallel, upload serial)
        // Call a helper `create_instance_primitives` here.

        if !all_primitives.is_empty() {
            todo!("Build BVH or KD-Tree")
        } else {
            todo!("Return empty")
        }
    }

    fn load_shapes_parallel(
        &self,
        entities: &[ShapeSceneEntity],
        lookup: &SceneLookup,
    ) -> Vec<Vec<Shape>> {
        entities
            .par_iter()
            .map(|sh| {
                Shape::create(
                    &sh.base.name,
                    sh.render_from_object.as_ref(),
                    sh.object_from_render.as_ref(),
                    sh.reverse_orientation,
                    &sh.base.parameters,
                    lookup.float_textures,
                    &sh.base.loc,
                )
            })
            .collect()
    }

    fn load_animated_shapes_parallel(
        &self,
        entities: &[AnimatedShapeSceneEntity],
        lookup: &SceneLookup,
    ) -> Vec<Vec<Shape>> {
        entities
            .par_iter()
            .map(|sh| {
                Shape::create(
                    &sh.transformed_base.base.name,
                    &sh.identity,
                    &sh.identity,
                    sh.reverse_orientation,
                    &sh.transformed_base.base.parameters,
                    lookup.float_textures,
                    &sh.transformed_base.base.loc,
                )
            })
            .collect()
    }

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

        for (i, (entity, shapes)) in entities.iter().zip(loaded_shapes).enumerate() {
            if shapes.is_empty() {
                continue;
            }

            let alpha_tex = self.get_alpha_texture(
                &entity.base.parameters,
                &entity.base.loc,
                arena,
                lookup.float_textures,
            );

            let mtl = lookup.resolve_material(&entity.material, &entity.base.loc);
            let mi = MediumInterface::new(
                lookup.find_medium(&entity.inside_medium, &entity.base.loc),
                lookup.find_medium(&entity.outside_medium, &entity.base.loc),
            );

            let shape_lights_opt = lookup.shape_lights.get(&i);

            for (j, shape_host) in shapes.into_iter().enumerate() {
                let mut area_light = None;
                if entity.light_index.is_some() {
                    if let Some(lights) = shape_lights_opt {
                        if j < lights.len() {
                            area_light = Some(lights[j]);
                        }
                    }
                }

                let shape_ptr = shape_host.upload(arena);

                let prim = if area_light.is_none()
                    && !mi.is_medium_transition()
                    && alpha_tex.is_none()
                {
                    let p = SimplePrimitive::new(shape_ptr, mtl.unwrap());
                    Primitive::Simple(arena.alloc(p))
                } else {
                    let p =
                        GeometricPrimitive::new(shape_ptr, mtl.unwrap(), area_light, mi, alpha_tex);
                    Primitive::Geometric(arena.alloc(p))
                };

                primitives.push(prim);
            }
        }
        primitives
    }

    fn upload_animated_shapes(
        &self,
        arena: &mut Arena,
        entities: &[AnimatedShapeSceneEntity],
        loaded_shapes: Vec<Vec<Shape>>,
        lookup: &SceneLookup,
    ) -> Vec<Primitive> {
        // Logic mirrors upload_shapes, but constructs AnimatedPrimitive or BVH
        // ...
        // Note: For AnimatedPrimitives, you wrap the result in `arena.alloc(AnimatedPrimitive::new(...))`
        Vec::new()
    }

    fn get_alpha_texture(
        &self,
        params: &ParameterDictionary,
        loc: &FileLoc,
        arena: &mut Arena,
        textures: &HashMap<String, FloatTexture>,
    ) -> Option<FloatTexture> {
        let alpha_name = params.get_texture("alpha");

        if let Some(name) = alpha_name {
            match textures.get(&name) {
                Some(tex) => Some(*tex),
                None => panic!("{:?}: Alpha texture '{}' not found", loc, name),
            }
        } else {
            let alpha_val = params.get_one_float("alpha", 1.0);
            if alpha_val < 1.0 {
                let tex = FloatConstantTexture::new(alpha_val);
                let ptr = arena.alloc(tex);
                Some(FloatTexture::Constant(ptr))
            } else {
                None
            }
        }
    }

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

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

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

    pub fn done(&self) {
        let shapes = self.shapes.lock().unwrap();
        let animated_shapes = self.animated_shapes.lock().unwrap();
        let instances = self.instances.lock().unwrap();
        let instance_defs = self.instance_definitions.lock().unwrap();

        let tex_state = self.texture_state.lock().unwrap();
        let mat_state = self.material_state.lock().unwrap();
        let med_state = self.media_state.lock().unwrap();
        let light_state = self.light_state.lock().unwrap();

        log::info!(
            "Scene stats: {} shapes, {} animated shapes, {} instance definitions, \
             {} instance uses, {} float textures, {} spectrum textures, \
             {} named materials, {} materials",
            shapes.len(),
            animated_shapes.len(),
            instance_defs.len(),
            instances.len(),
            // Count serial + async jobs for total textures
            tex_state.serial_float_textures.len() + tex_state.float_texture_jobs.len(),
            tex_state.serial_spectrum_textures.len()
                + tex_state.async_spectrum_textures.len()
                + tex_state.spectrum_texture_jobs.len(),
            mat_state.named_materials.len(),
            mat_state.materials.len()
        );

        {
            let mut unused_float_tex: HashSet<&str> = HashSet::new();
            let mut unused_spec_tex: HashSet<&str> = HashSet::new();

            for (name, _) in &tex_state.serial_float_textures {
                unused_float_tex.insert(name);
            }
            for name in tex_state.float_texture_jobs.keys() {
                unused_float_tex.insert(name);
            }

            for (name, _) in &tex_state.serial_spectrum_textures {
                unused_spec_tex.insert(name);
            }
            for (name, _) in &tex_state.async_spectrum_textures {
                unused_spec_tex.insert(name);
            }
            for name in tex_state.spectrum_texture_jobs.keys() {
                unused_spec_tex.insert(name);
            }

            // Define the Checker Closure
            let mut check_params = |params: &ParameterDictionary| {
                for param in &params.params {
                    if param.type_name == "texture" {
                        // Textures are usually stored in `strings`
                        for tex_name in &param.strings {
                            if unused_float_tex.contains(tex_name.as_str()) {
                                unused_float_tex.remove(tex_name.as_str());
                            } else if unused_spec_tex.contains(tex_name.as_str()) {
                                unused_spec_tex.remove(tex_name.as_str());
                            }
                        }
                    }
                }
            };

            // Materials
            for (_, mat) in &mat_state.named_materials {
                check_params(&mat.parameters);
            }
            for mat in &mat_state.materials {
                check_params(&mat.parameters);
            }

            for (_, tex) in &tex_state.serial_float_textures {
                check_params(&tex.parameters);
            }
            for (_, tex) in &tex_state.serial_spectrum_textures {
                check_params(&tex.parameters);
            }

            // Shapes
            for shape in shapes.iter() {
                check_params(&shape.parameters);
            }
            for anim_shape in animated_shapes.iter() {
                check_params(&anim_shape.parameters);
            }

            // Instance Definitions
            for def in instance_defs.values() {
                for shape in &def.shapes {
                    check_params(&shape.parameters);
                }
                for anim_shape in &def.animated_shapes {
                    check_params(&anim_shape.parameters);
                }
            }

            for name in unused_float_tex {
                log::warn!("{}: float texture unused in scene", name);
            }
            for name in unused_spec_tex {
                log::warn!("{}: spectrum texture unused in scene", name);
            }
        }
    }

    // PRIVATE METHODS
    fn get_singleton<T: Send + Sync + 'static>(
        &self,
        mutex: &Mutex<SingletonState<T>>,
        name: &str,
    ) -> Arc<T> {
        let mut state = mutex.lock().unwrap();

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

        if let Some(job) = state.job.take() {
            let res = Arc::new(job.wait());
            state.result = Some(res.clone());
            log::debug!("Retrieved {} from future", name);
            return res;
        }

        panic!("{} requested but not initialized!", name);
    }

    fn start_loading_normal_maps(
        &self,
        state: &mut MaterialState,
        parameters: &ParameterDictionary,
    ) {
        let filename = resolve_filename(parameters.get_one_string("filename", ""));
        if filename.is_empty() {
            log::error!("Could not load normal maps");
            return;
        }

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

        let filename_clone = filename.clone();

        let job = run_async(move || {
            let path = std::path::Path::new(&filename_clone);

            let immeta = Image::read(path, Some(ColorEncoding::Linear))
                .unwrap_or_else(|e| panic!("{}: unable to read normal map: {}", filename_clone, e));

            let image = &immeta.image;

            let rgb_desc = image.get_channel_desc(&["R", "G", "B"]).unwrap_or_else(|| {
                panic!(
                    "{}: normal map must contain R, G, and B channels",
                    filename_clone
                )
            });

            let normal_map = image.select_channels(&rgb_desc);

            Arc::new(normal_map)
        });

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

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

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

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

        if let Some(job) = state.jobs.remove(name) {
            let medium = Arc::new(job.wait());
            state.map.insert(name.to_string(), medium.clone());

            return Some(medium);
        }

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

        None
    }
}

const MAX_TRANSFORMS: usize = 2;

#[derive(Debug, Default, Clone, Copy)]
struct TransformSet {
    t: [Transform; MAX_TRANSFORMS],
}

impl TransformSet {
    pub fn is_animated(&self) -> bool {
        self.t[0] != self.t[1]
    }

    pub fn inverse(&self) -> Self {
        Self {
            t: [self.t[0].inverse(), self.t[1].inverse()],
        }
    }

    pub fn map<F>(&mut self, bits: u32, f: F)
    where
        F: Fn(&Transform) -> Transform,
    {
        if (bits & 1) != 0 {
            self.t[0] = f(&self.t[0]);
        }
        if (bits & 2) != 0 {
            self.t[1] = f(&self.t[1]);
        }
    }
}

impl IndexTrait<usize> for TransformSet {
    type Output = Transform;

    fn index(&self, index: usize) -> &Self::Output {
        &self.t[index]
    }
}

impl IndexMutTrait<usize> for TransformSet {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.t[index]
    }
}

#[derive(Default, Debug, Clone)]
struct GraphicsState {
    pub current_inside_medium: String,
    pub current_outside_medium: String,
    pub current_material_name: String,
    pub current_material_index: Option<usize>,
    pub area_light_name: String,
    pub area_light_params: ParsedParameterVector,
    pub area_light_loc: FileLoc,
    pub shape_attributes: ParsedParameterVector,
    pub light_attributes: ParsedParameterVector,
    pub material_attributes: ParsedParameterVector,
    pub medium_attributes: ParsedParameterVector,
    pub texture_attributes: ParsedParameterVector,

    pub reverse_orientation: bool,
    pub color_space: Option<Arc<RGBColorSpace>>,

    pub ctm: TransformSet,
    // 1=Start, 2=End, 3=All
    pub active_transform_bits: u32,
    pub transform_start_time: Float,
    pub transform_end_time: Float,
}

#[derive(PartialEq, Eq)]
enum BlockState {
    OptionsBlock,
    WorldBlock,
}

pub struct BasicSceneBuilder {
    scene: Arc<BasicScene>,
    current_block: BlockState,
    graphics_state: GraphicsState,
    pushed_graphics_states: Vec<GraphicsState>,
    push_stack: Vec<(char, FileLoc)>,
    render_from_world: Transform,
    named_coordinate_systems: HashMap<String, TransformSet>,
    active_instance_definition: Option<InstanceDefinitionSceneEntity>,

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

    named_material_names: HashSet<String>,
    medium_names: HashSet<String>,
    current_camera: Option<CameraSceneEntity>,
    current_film: Option<SceneEntity>,
    current_integrator: Option<SceneEntity>,
    current_sampler: Option<SceneEntity>,
    current_filter: Option<SceneEntity>,
    current_accelerator: Option<SceneEntity>,
}

impl BasicSceneBuilder {
    pub const START_TRANSFORM_BITS: u32 = 1 << 0;
    pub const END_TRANSFORM_BITS: u32 = 1 << 1;
    pub const ALL_TRANSFORM_BITS: u32 = (1 << MAX_TRANSFORMS) - 1;

    pub fn new(scene: Arc<BasicScene>) -> Self {
        Self {
            scene,
            current_block: BlockState::OptionsBlock,
            graphics_state: GraphicsState::default(),
            pushed_graphics_states: Vec::new(),
            push_stack: Vec::new(),
            render_from_world: Transform::identity(),
            named_coordinate_systems: HashMap::new(),
            active_instance_definition: None,
            shapes: Vec::new(),
            animated_shapes: Vec::new(),
            named_material_names: HashSet::new(),
            medium_names: HashSet::new(),

            current_camera: Some(CameraSceneEntity {
                base: SceneEntity {
                    name: "perspective".into(),
                    ..Default::default()
                },
                camera_transform: CameraTransform::from_world(
                    AnimatedTransform::default(),
                    RenderingCoordinateSystem::World,
                ),
                medium: String::new(),
            }),
            current_sampler: Some(SceneEntity {
                name: "zsobol".into(),
                ..Default::default()
            }),
            current_filter: Some(SceneEntity {
                name: "gaussian".into(),
                ..Default::default()
            }),
            current_integrator: Some(SceneEntity {
                name: "volpath".into(),
                ..Default::default()
            }),
            current_accelerator: Some(SceneEntity {
                name: "bvh".into(),
                ..Default::default()
            }),
            current_film: Some(SceneEntity {
                name: "rgb".into(),
                ..Default::default()
            }),
        }
    }

    fn for_active_transforms<F>(&mut self, f: F)
    where
        F: Fn(&Transform) -> Transform,
    {
        let bits = self.graphics_state.active_transform_bits;

        if (bits & 1) != 0 {
            self.graphics_state.ctm.t[0] = f(&self.graphics_state.ctm.t[0]);
        }
        if (bits & 2) != 0 {
            self.graphics_state.ctm.t[1] = f(&self.graphics_state.ctm.t[1]);
        }
    }

    fn verify_world(&self, name: &str, loc: &FileLoc) {
        if self.current_block != BlockState::WorldBlock {
            eprintln!("Error: {} not allowed outside WorldBlock at {}", name, loc);
        }
    }

    fn verify_options(&self, name: &str, loc: &FileLoc) {
        if self.current_block != BlockState::OptionsBlock {
            eprintln!("Error: {} not allowed inside WorldBlock at {}", name, loc);
        }
    }
}

impl ParserTarget for BasicSceneBuilder {
    fn reverse_orientation(&mut self, loc: FileLoc) {
        self.verify_world("ReverseOrientation", &loc);
        self.graphics_state.reverse_orientation = !self.graphics_state.reverse_orientation;
    }

    fn color_space(&mut self, name: &str, loc: FileLoc) {
        let _ = match RGBColorSpace::get_named(name) {
            Ok(cs) => {
                self.graphics_state.color_space = Some(cs);
            }
            Err(_) => {
                eprintln!("Error: Color space '{}' unknown at {}", name, loc);
            }
        };
    }

    fn identity(&mut self, _loc: FileLoc) {
        self.for_active_transforms(|_| Transform::identity());
    }

    fn translate(&mut self, dx: Float, dy: Float, dz: Float, _loc: FileLoc) {
        let t = Transform::translate(Vector3f::new(dx, dy, dz));
        self.for_active_transforms(|cur| cur * &t);
    }

    fn rotate(&mut self, angle: Float, ax: Float, ay: Float, az: Float, _loc: FileLoc) {
        let t = Transform::rotate_around_axis(angle, Vector3f::new(ax, ay, az));
        self.for_active_transforms(|cur| cur * &t);
    }

    fn scale(&mut self, sx: Float, sy: Float, sz: Float, _loc: FileLoc) {
        let t = Transform::scale(sx, sy, sz);
        self.for_active_transforms(|cur| cur * &t);
    }

    fn look_at(
        &mut self,
        ex: Float,
        ey: Float,
        ez: Float,
        lx: Float,
        ly: Float,
        lz: Float,
        ux: Float,
        uy: Float,
        uz: Float,
        loc: FileLoc,
    ) {
        let result = look_at((ex, ey, ez), (lx, ly, lz), (ux, uy, uz));
        match result {
            Ok(t) => {
                self.for_active_transforms(|cur| cur * &t);
            }
            Err(e) => {
                eprintln!("Error: {} at {}", e, loc);
            }
        }
    }

    fn concat_transform(&mut self, m: &[Float; 16], loc: FileLoc) {
        let result = Transform::from_flat(m);
        match result {
            Ok(t) => {
                self.for_active_transforms(|cur| cur * &t);
            }
            Err(e) => {
                eprintln!("Error: {} at {}", e, loc);
            }
        }
    }

    fn transform(&mut self, m: &[Float; 16], loc: FileLoc) {
        let result = Transform::from_flat(m);
        match result {
            Ok(t) => {
                self.for_active_transforms(|_| t);
            }
            Err(e) => {
                eprintln!("Error: {} at {}", e, loc);
            }
        }
    }

    fn coordinate_system(&mut self, name: &str, _loc: FileLoc) {
        self.named_coordinate_systems
            .insert(name.to_string(), self.graphics_state.ctm);
    }

    fn coord_sys_transform(&mut self, name: &str, loc: FileLoc) {
        if let Some(saved_ctm) = self.named_coordinate_systems.get(name) {
            self.graphics_state.ctm = *saved_ctm;
        } else {
            eprintln!(
                "Warning: Couldn't find named coordinate system \"{}\" at {}",
                name, loc
            );
        }
    }

    fn camera(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        self.verify_options("Camera", &loc);

        let camera_from_world = self.graphics_state.ctm;

        let world_from_camera = camera_from_world.inverse();

        self.named_coordinate_systems
            .insert("camera".to_string(), world_from_camera);

        let animated_world_from_cam = AnimatedTransform::new(
            &world_from_camera.t[0],
            self.graphics_state.transform_start_time,
            &world_from_camera.t[1],
            self.graphics_state.transform_end_time,
        );

        let rendering_space = RenderingCoordinateSystem::CameraWorld;
        let camera_transform =
            CameraTransform::from_world(animated_world_from_cam, rendering_space);
        self.render_from_world = camera_from_world.t[0];

        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());

        self.current_camera = Some(CameraSceneEntity {
            base: SceneEntity {
                name: name.to_string(),
                loc,
                parameters,
            },
            camera_transform,

            medium: self.graphics_state.current_outside_medium.clone(),
        });
    }

    fn active_transform_all(&mut self, _loc: FileLoc) {
        self.graphics_state.active_transform_bits = Self::ALL_TRANSFORM_BITS;
    }

    fn active_transform_end_time(&mut self, _loc: FileLoc) {
        self.graphics_state.active_transform_bits = Self::END_TRANSFORM_BITS;
    }
    fn active_transform_start_time(&mut self, _loc: FileLoc) {
        self.graphics_state.active_transform_bits = Self::START_TRANSFORM_BITS;
    }

    fn transform_times(&mut self, start: Float, end: Float, loc: FileLoc) {
        self.verify_options("TransformTimes", &loc);
        self.graphics_state.transform_start_time = start;
        self.graphics_state.transform_end_time = end;
    }

    fn option(&mut self, _name: &str, _value: &str, _loc: FileLoc) {
        todo!()
    }

    fn pixel_filter(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());
        self.verify_options("PixelFilter", &loc);
        self.current_filter = Some(SceneEntity {
            name: name.to_string(),
            loc,
            parameters,
        });
    }

    fn film(&mut self, type_name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());
        self.verify_options("Film", &loc);
        self.current_filter = Some(SceneEntity {
            name: type_name.to_string(),
            loc,
            parameters,
        });
    }

    fn accelerator(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());
        self.verify_options("PixelFilter", &loc);
        self.current_filter = Some(SceneEntity {
            name: name.to_string(),
            loc,
            parameters,
        });
    }

    fn integrator(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());
        self.verify_options("PixelFilter", &loc);
        self.current_filter = Some(SceneEntity {
            name: name.to_string(),
            loc,
            parameters,
        });
    }

    fn make_named_medium(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        let curr_name = normalize_utf8(name);
        self.verify_world("MakeNamedMaterial", &loc);

        if !self.named_material_names.insert(curr_name.to_string()) {
            eprintln!("Error: {}: named material '{}' redefined.", loc, name);
            return;
        }

        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());

        let entity = SceneEntity {
            name: name.to_string(),
            loc,
            parameters,
        };

        self.scene
            .named_materials
            .lock()
            .push((curr_name.to_string(), entity));
    }

    fn medium_interface(&mut self, inside_name: &str, outside_name: &str, _loc: FileLoc) {
        let inside = normalize_utf8(inside_name);
        let outside = normalize_utf8(outside_name);

        self.graphics_state.current_inside_medium = inside;
        self.graphics_state.current_outside_medium = outside;
    }

    fn sampler(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        let parameters =
            ParameterDictionary::new(params.clone(), self.graphics_state.color_space.clone());
        self.verify_options("Sampler", &loc);
        self.current_sampler = Some(SceneEntity {
            name: name.to_string(),
            loc,
            parameters,
        })
    }

    fn world_begin(&mut self, loc: FileLoc) {
        self.verify_options("WorldBegin", &loc);
        self.current_block = BlockState::WorldBlock;
        for i in 0..MAX_TRANSFORMS {
            self.graphics_state.ctm[i] = Transform::default();
        }
        self.graphics_state.active_transform_bits = Self::ALL_TRANSFORM_BITS;
        self.named_coordinate_systems
            .insert("world".to_string(), self.graphics_state.ctm);
        let scene = Arc::clone(&self.scene);
        scene.set_options(
            self.current_filter
                .take()
                .expect("Filter not set before WorldBegin"),
            self.current_film
                .take()
                .expect("Film not set before WorldBegin"),
            self.current_camera
                .take()
                .expect("Camera not set before WorldBegin"),
            self.current_sampler
                .take()
                .expect("Sampler not set before WorldBegin"),
            self.current_integrator
                .take()
                .expect("Integrator not set before WorldBegin"),
            self.current_accelerator
                .take()
                .expect("Accelerator not set before WorldBegin"),
        );
    }

    fn attribute_begin(&mut self, loc: FileLoc) {
        self.verify_world("AttributeBegin", &loc);
        self.pushed_graphics_states
            .push(self.graphics_state.clone());
        self.push_stack.push(('a', loc));
    }

    fn attribute_end(&mut self, loc: FileLoc) {
        self.verify_world("AttributeEnd", &loc);
        if self.pushed_graphics_states.is_empty() {
            log::error!(
                "[{:?}] Unmatched AttributeEnd encountered. Ignoring it.",
                loc
            );
            return;
        }

        if let Some(state) = self.pushed_graphics_states.pop() {
            self.graphics_state = state;
        }

        if let Some((kind, start_loc)) = self.push_stack.pop() {
            if kind == 'o' {
                log::error!(
                    "[{:?}] Mismatched nesting: open ObjectBegin from {} at AttributeEnd",
                    loc,
                    start_loc
                );
                std::process::exit(1);
            } else {
                debug_assert_eq!(kind, 'a', "Expected AttributeBegin on the stack");
            }
        }
    }

    fn attribute(&mut self, target: &str, params: ParsedParameterVector, loc: FileLoc) {
        let current_attributes = match target {
            "shape" => &mut self.graphics_state.shape_attributes,
            "light" => &mut self.graphics_state.light_attributes,
            "material" => &mut self.graphics_state.material_attributes,
            "medium" => &mut self.graphics_state.medium_attributes,
            "texture" => &mut self.graphics_state.texture_attributes,
            _ => {
                log::error!(
                    "[{:?}] Unknown attribute target \"{}\". Must be \"shape\", \"light\", \
                \"material\", \"medium\", or \"texture\".",
                    loc,
                    target
                );
                return;
            }
        };

        let active_color_space = self.graphics_state.color_space.clone();

        for mut p in params {
            p.may_be_unused = true;
            p.color_space = active_color_space.clone();

            current_attributes.push(p.clone());
        }
    }

    fn texture(
        &mut self,
        orig_name: &str,
        type_name: &str,
        tex_name: &str,
        params: &ParsedParameterVector,
        loc: FileLoc,
    ) {
        let name = normalize_utf8(orig_name);
        self.verify_world("Texture", &loc);
        let dict = ParameterDictionary::from_array(
            params.clone(),
            &self.graphics_state.texture_attributes,
            self.graphics_state.color_space.clone(),
        );

        if type_name != "float" && type_name != "spectrum" {
            self.error_exit_deferred(
                &loc,
                &format!(
                    "{}: texture type unknown. Must be \"float\" or \"spectrum\".",
                    tex_name
                ),
            );
            return;
        }

        {
            let names = if type_name == "float" {
                &mut self.float_texture_names
            } else {
                &mut self.spectrum_texture_names
            };

            if names.contains(name) {
                self.error_exit_deferred(&loc, &format!("Redefining texture \"{}\".", name));
                return;
            }
            names.insert(name.to_string());
        }

        let base = SceneEntity {
            name: tex_name,
            parameters: dict,
            loc,
        };
        let entity = TextureSceneEntity {
            base,
            render_from_object: self.graphics_state.render_from_object.clone(),
        };

        if type_name == "float" {
            self.scene.add_float_texture(name.to_string(), entity);
        } else {
            self.scene.add_spectrum_texture(name.to_string(), entity);
        }
    }

    fn material(&mut self, name: &str, params: &ParsedParameterVector, loc: FileLoc) {
        self.verify_world("material", loc);
        let entity = SceneEntity {
            name,
            loc,
            parameters: params,
        };
        self.graphics_state.current_material_name = self.scene.add_material(name, entity);
    }
    fn make_named_material(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) {
        todo!()
    }
    fn named_material(&mut self, _name: &str, _loc: FileLoc) {
        todo!()
    }
    fn light_source(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) {
        todo!()
    }
    fn area_light_source(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) {
        todo!()
    }
    fn shape(&mut self, _name: &str, _params: &ParsedParameterVector, _loc: FileLoc) {
        todo!()
    }
    fn object_begin(&mut self, _name: &str, _loc: FileLoc) {
        todo!()
    }
    fn object_end(&mut self, _loc: FileLoc) {
        todo!()
    }
    fn object_instance(&mut self, _name: &str, _loc: FileLoc) {
        todo!()
    }
    fn end_of_files(&mut self) {
        todo!()
    }
}
