use crate::core::image::Image;
use crate::core::material::CreateMaterial;
use crate::core::texture::SpectrumTexture;
use crate::globals::get_options;
use crate::spectra::data::get_named_spectrum;
use crate::utils::TextureParameterDictionary;
use crate::{Arena, FileLoc, Upload, ArenaUpload};
use anyhow::{bail, Result};
use shared::core::material::Material;
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType;
use shared::materials::coated::*;
use shared::spectra::ConstantSpectrum;
use shared::textures::SpectrumConstantTexture;
use std::collections::HashMap;
use std::sync::Arc;

impl CreateMaterial for CoatedDiffuseMaterial {
    fn create(
        parameters: &TextureParameterDictionary,
        normal_map: Option<Arc<Image>>,
        _named_materials: &HashMap<String, Material>,
        _loc: &FileLoc,
        arena: &Arena,
    ) -> Result<Material> {
        let reflectance = parameters
            .get_spectrum_texture("reflectance", None, SpectrumType::Albedo)
            .unwrap_or(Arc::new(SpectrumTexture::Constant(
                SpectrumConstantTexture::new(Spectrum::Constant(ConstantSpectrum::new(0.5))),
            )));

        let u_roughness =
            parameters.get_float_texture_with_fallback("uroughness", "roughness", 0.5)?;

        let v_roughness =
            parameters.get_float_texture_with_fallback("vroughness", "roughness", 0.5)?;

        let thickness = parameters.get_float_texture("thickness", 0.01)?;
        let eta = parameters
            .get_float_array("eta")?
            .first()
            .map(|&v| Spectrum::Constant(ConstantSpectrum::new(v)))
            .or_else(|| parameters.get_one_spectrum("eta", None, SpectrumType::Unbounded))
            .unwrap_or_else(|| Spectrum::Constant(ConstantSpectrum::new(1.5)));

        let max_depth = parameters.get_one_int("maxdepth", 10)?;
        let n_samples = parameters.get_one_int("nsamples", 1)?;
        let g = parameters.get_float_texture("g", 0.)?;
        let albedo = parameters
            .get_spectrum_texture("albedo", None, SpectrumType::Albedo)
            .unwrap_or_else(|| {
                let default_spectrum = Spectrum::Constant(ConstantSpectrum::new(0.));
                SpectrumTexture::Constant(SpectrumConstantTexture::new(default_spectrum)).into()
            });
        let displacement = parameters.get_float_texture("displacement", 0.)?;
        let remap_roughness = parameters.get_one_bool("remaproughness", true)?;

        let specific = CoatedDiffuseMaterial::new(
            arena.upload(reflectance),
            arena.upload(u_roughness),
            arena.upload(v_roughness),
            arena.upload(thickness),
            arena.upload(albedo),
            arena.upload(g),
            arena.upload(displacement),
            arena.alloc(eta),
            arena.alloc(normal_map),
            remap_roughness,
            max_depth as u32,
            n_samples as u32,
            get_options().seed,
        );

        Ok(Material::CoatedDiffuse(specific))
    }
}

impl CreateMaterial for CoatedConductorMaterial {
    fn create(
        parameters: &TextureParameterDictionary,
        normal_map: Option<Arc<Image>>,
        _named_materials: &HashMap<String, Material>,
        loc: &FileLoc,
        arena: &Arena,
    ) -> Result<Material> {
        let interface_u_roughness = parameters.get_float_texture_with_fallback(
            "interface.uroughness",
            "interface.roughness",
            0.,
        )?;
        let interface_v_roughness = parameters.get_float_texture_with_fallback(
            "interface.vroughness",
            "interface.roughness",
            0.,
        )?;

        let thickness = parameters.get_float_texture("interface.thickness", 0.01)?;
        let interface_eta: Spectrum = parameters
            .get_float_array("interface.eta")?
            .first()
            .map(|&v| Spectrum::Constant(ConstantSpectrum::new(v)))
            .or_else(|| parameters.get_one_spectrum("interface.eta", None, SpectrumType::Unbounded))
            .unwrap_or_else(|| Spectrum::Constant(ConstantSpectrum::new(1.5)));

        let conductor_u_roughness = parameters.get_float_texture_with_fallback(
            "conductor.uroughness",
            "conductor.roughness",
            0.,
        )?;

        let conductor_v_roughness = parameters.get_float_texture_with_fallback(
            "conductor.vroughness",
            "conductor.roughness",
            0.,
        )?;

        let reflectance =
            parameters.get_spectrum_texture_or_null("reflectance", SpectrumType::Albedo);
        let conductor_eta =
            parameters.get_spectrum_texture_or_null("conductor.eta", SpectrumType::Unbounded);
        let k = parameters.get_spectrum_texture_or_null("conductor.k", SpectrumType::Unbounded);

        let (conductor_eta, k) = if reflectance.is_some() {
            if conductor_eta.is_some() || k.is_some() {
                bail!(
                    "{}: For the coated conductor material, both \"reflectance\" \
         and \"eta\"/\"k\" cannot be provided simultaneously.",
                    loc
                );
            }
            (None, None)
        } else {
            let eta = conductor_eta.unwrap_or_else(|| {
                let s = get_named_spectrum("metal-Cu-eta").expect("Missing copper spectrum");
                Arc::new(SpectrumTexture::Constant(SpectrumConstantTexture::new(s)))
            });
            let k = k.unwrap_or_else(|| {
                let s = get_named_spectrum("metal-Cu-k").expect("Missing copper spectrum");
                Arc::new(SpectrumTexture::Constant(SpectrumConstantTexture::new(s)))
            });
            (Some(eta), Some(k))
        };

        let max_depth = parameters.get_one_int("maxdepth", 10)?;
        let n_samples = parameters.get_one_int("nsamples", 1)?;
        let g = parameters.get_float_texture("g", 0.)?;
        let albedo = parameters
            .get_spectrum_texture("albedo", None, SpectrumType::Albedo)
            .unwrap_or_else(|| {
                let spectrum = Spectrum::Constant(ConstantSpectrum::new(0.));
                SpectrumTexture::Constant(SpectrumConstantTexture::new(spectrum)).into()
            });
        let displacement = parameters.get_float_texture_or_null("displacement")?;
        let remap_roughness = parameters.get_one_bool("remaproughness", true)?;

        let material = Self::new(
            arena.upload(displacement)
            arena.upload(interface_u_roughness),
            arena.upload(interface_v_roughness),
            arena.upload(thickness),
            arena.upload(g),
            arena.upload(albedo),
            arena.upload(conductor_u_roughness),
            arena.upload(conductor_v_roughness),
            arena.upload(conductor_eta),
            arena.upload(k),
            arena.upload(reflectance),
            arena.alloc(normal_map),
            arena.alloc(interface_eta),
            max_depth as u32,
            n_samples as u32,
            remap_roughness,
            get_options().seed,
        );
        arena.alloc(material);
        Ok(Material::CoatedConductor(material))
    }
}
