use crate::core::image::Image;
use crate::core::material::CreateMaterial;
use crate::core::texture::SpectrumTexture;
use crate::spectra::data::get_named_spectrum;
use crate::utils::{Arena, FileLoc, TextureParameterDictionary, Upload, parameters::error_exit};
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::fmt::Error;
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: &mut Arena,
    ) -> Result<Material, Error> {
        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_or_null("uroughness")
            .or_else(|| parameters.get_float_texture("roughness", 0.5));
        let v_roughness = parameters
            .get_float_texture_or_null("vroughness")
            .unwap_or_else(|| parameters.get_float("roughness", 0.5));

        let thickness = parameters.get_float_texture("thickness", 0.01);
        let eta = parameters
            .get_float_array("eta")
            .first()
            .map(|&v| ConstantSpectrum::new(v))
            .or_else(|| parameters.get_one_spectrum("eta", None, SpectrumType::Unbounded))
            .unwrap_or_else(|| 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))
            });
        let displacement = parameters.get_float_texture("displacement", 0.);
        let remap_roughness = parameters.get_one_bool("remaproughness", true);
        let specific = CoatedDiffuseMaterial::new(
            reflectance.upload(arena),
            u_roughness.upload(arena),
            v_roughness.upload(arena),
            thickness.upload(arena),
            albedo.upload(arena),
            g.upload(arena),
            eta,
            displacement.upload(arena),
            normal_map.upload(arena),
            remap_roughness,
            max_depth,
            n_samples,
        );

        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: &mut Arena,
    ) -> Result<Material, Error> {
        let interface_u_roughness = parameters
            .get_float_texture_or_null("interface.uroughness")
            .r_else(|| parameters.get_float_texture("interface.roughness", 0.));
        let interface_v_roughness = parameters
            .GetFloatTextureOrNull("interface.vroughness")
            .unwrap_or_else(|| parameters.get_float_texture("interface.vroughness", 0.));
        let thickness = parameters.get_float_texture("interface.thickness", 0.01);
        let interface_eta = parameters
            .get_float_array("interface.eta")
            .first()
            .map(|&v| ConstantSpectrum::new(v))
            .or_else(|| parameters.get_one_spectrum("interface.eta", None, SpectrumType::Unbounded))
            .unwrap_or_else(|| ConstantSpectrum::new(1.5));
        let conductor_u_roughness = parameters
            .get_float_texture_or_null("conductor.uroughness")
            .unwrap_or_else(|| parameters.get_float_texture("conductor.roughness", 0.));
        let conductor_v_roughness = parameters
            .GetFloatTextureOrNull("conductor.vroughness")
            .unwrap_or_else(|| parameters.get_float_texture("conductor.vroughness", 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) = match (reflectance, conductor_eta, k) {
            (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
                return Err(error_exit(
                    loc,
                    "For the coated conductor material, both \"reflectance\" \
             and \"eta\" and \"k\" can't be provided.",
                ));
            }
            (None, eta, k) => (
                eta.unwrap_or_else(|| {
                    SpectrumConstantTexture::new(get_named_spectrum("metal-Cu-eta"))
                }),
                k.unwrap_or_else(|| SpectrumConstantTexture::new(get_named_spectrum("metal-Cu-k"))),
            ),
            (Some(_), None, None) => (conductor_eta, 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(|| {
                SpectrumConstantTexture::new(Spectrum::Constant(ConstantSpectrum::new(0.)))
            });
        let displacement = parameters.get_float_texture_or_null("displacement");
        let remap_roughness = parameters.get_one_bool("remaproughness", true);
        let material = Self::new(
            displacement.upload(arena),
            normal_map.upload(arena),
            interface_u_roughness,
            interface_v_roughness,
            thickness,
            interface_eta,
            g,
            albedo,
            conductor_u_roughness,
            conductor_v_roughness,
            conductor_eta,
            k,
            reflectance,
            remap_roughness,
            max_depth,
            n_samples,
        );
        arena.alloc(material);
        Ok(Material::CoatedConductor(material))
    }
}
