use ray_tracing_core::{
    color::Color, material::SampleResult, math::material::fresnel_real, prelude::*,
};

use crate::{
    either::Either,
    texture::{Pbrt2dFloatTexture, Pbrt2dSpectrumTexture, parse_rgb},
};

use super::*;

pub trait PbrtMaterial<R: Rng>: std::fmt::Debug + Send + Sync {
    /// evaluate f(w_in, w_out)
    fn eval(&self, u: Float, v: Float, w_in: Dir3, w_out: Dir3, rng: &mut R) -> Color;

    /// sample w_out and return:
    /// f(w_in, w_out) * cos(theta) / pdf
    fn sample(&self, u: Float, v: Float, w_in: Dir3, rng: &mut R) -> SampleResult {
        let w_out = Dir3::sample_cosine_hemisphere(rng);

        SampleResult::new(
            w_out,
            self.eval(u, v, w_in, w_out, rng) * FloatConsts::PI,
            false,
        )
    }

    /// returns the pdf for (w_in, w_out)
    fn pdf(&self, u: Float, v: Float, w_in: Dir3, w_out: Dir3) -> Float {
        let _ = v;
        let _ = u;
        let _ = w_out;
        FloatConsts::FRAC_1_PI * Float::max(w_in.y(), 0.0)
    }
}

pub fn parse_make_named_material<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<(String, Arc<dyn PbrtMaterial<R>>)> {
    let name = input.next_string_value()?;

    if input.next_string_value()?.as_str() != "string type" {
        return Err(miette!(
            "first element of make named material dict has to be type"
        ));
    }
    Ok((name, parse_material(input, context)?))
}

#[derive(Debug)]
#[allow(dead_code)]
struct PbrtCoatedDiffuseMaterial {
    albedo: Either<Color, Arc<dyn Pbrt2dSpectrumTexture>>,
    g: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
    maxdepth: usize,
    nsamples: usize,
    thickness: Float,
    roughness: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
    uroughness: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
    vroughness: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
    reflectance: Either<Color, Arc<dyn Pbrt2dSpectrumTexture>>,
    #[allow(clippy::type_complexity)]
    eta: Either<
        Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
        Either<Color, Arc<dyn Pbrt2dSpectrumTexture>>,
    >,
}

impl<R: Rng> PbrtMaterial<R> for PbrtCoatedDiffuseMaterial {
    fn eval(&self, u: Float, v: Float, w_in: Dir3, w_out: Dir3, rng: &mut R) -> Color {
        let _ = rng;
        let _ = w_in;
        if w_out.y() >= 0.0 {
            match &self.reflectance {
                Either::A(v) => *v,
                Either::B(t) => t.get(u, v),
            }
        } else {
            Color::black()
        }
    }
}

#[derive(Debug)]
#[allow(dead_code)]
struct PbrtDielectricMaterial {
    #[allow(clippy::type_complexity)]
    eta: Either<
        Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
        Either<Color, Arc<dyn Pbrt2dSpectrumTexture>>,
    >,
    roughness: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
    uroughness: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
    vroughness: Either<Float, Arc<dyn Pbrt2dFloatTexture>>,
}

impl<R: Rng> PbrtMaterial<R> for PbrtDielectricMaterial {
    fn eval(&self, u: Float, v: Float, w_in: Dir3, w_out: Dir3, rng: &mut R) -> Color {
        let _ = rng;
        let _ = w_out;
        let _ = w_in;
        let _ = v;
        let _ = u;
        Color::black()
    }

    fn sample(&self, u: Float, v: Float, w_in: Dir3, rng: &mut R) -> SampleResult {
        let _ = rng;
        let _ = v;
        let _ = u;
        let eta = match self.eta {
            Either::A(Either::A(eta)) => eta,
            _ => todo!(),
        };

        let (n1, n2) = if w_in.y() >= 0.0 {
            (1.0, eta)
        } else {
            (eta, 1.0)
        };

        let r = fresnel_real(w_in.y().abs(), n1, n2);

        if r >= rng.r#gen() {
            SampleResult::new(
                Dir3::new(0.0, 2.0 * w_in.y(), 0.0) - w_in,
                Color::white(),
                true,
            )
        } else {
            let r = n1 / n2;
            let w_out = -w_in * r
                + (r * w_in.y().abs() - Float::sqrt(1.0 - r * r * (1.0 - w_in.y() * w_in.y())))
                    * Dir3::new(0.0, if w_in.y() > 0.0 { 1.0 } else { -1.0 }, 0.0);

            SampleResult::new(w_out, Color::white(), true)
        }
    }

    fn pdf(&self, u: Float, v: Float, w_in: Dir3, w_out: Dir3) -> Float {
        let _ = w_in;
        let _ = v;
        let _ = u;
        let _ = w_out;
        0.0
    }
}

#[derive(Debug)]
#[allow(dead_code)]
struct PbrtDiffuseMaterial {
    reflectance: Either<Color, Arc<dyn Pbrt2dSpectrumTexture>>,
}

impl<R: Rng> PbrtMaterial<R> for PbrtDiffuseMaterial {
    fn eval(&self, u: Float, v: Float, w_in: Dir3, w_out: Dir3, rng: &mut R) -> Color {
        let _ = rng;
        let _ = w_in;
        if w_out.y() >= 0.0 {
            match &self.reflectance {
                Either::A(v) => *v,
                Either::B(t) => t.get(u, v),
            }
        } else {
            Color::black()
        }
    }
}

fn parse_2d_float_texture<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<Arc<dyn Pbrt2dFloatTexture>> {
    let n = input.next_string_value()?;

    context
        .get_texture(&n)
        .ok_or_else(|| miette!("Unknown texture"))
        .and_then(|t| Arc::clone(t).get_2d_float_texture())
}

fn parse_2d_spectrum_texture<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<Arc<dyn Pbrt2dSpectrumTexture>> {
    let n = input.next_string_value()?;

    context
        .get_texture(&n)
        .ok_or_else(|| miette!("Unknown texture"))
        .and_then(|t| Arc::clone(t).get_2d_spectrum_texture())
}

pub fn parse_material<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<Arc<dyn PbrtMaterial<R>>> {
    let material: String = input.next_string_value()?;

    match material.as_str() {
        "diffuse" => Ok(Arc::new(parse_dict2!(input, PbrtDiffuseMaterial;
            reflectance, Either::A(Color::black()), [
                "rgb reflectance", Either::A(texture::parse_rgb(input)?);
                "spectrum reflectance", Either::A(texture::parse_spectrum(input)?);
                "texture reflectance", Either::B(parse_2d_spectrum_texture(input, context)?)
            ]
        ))),
        "coateddiffuse" => Ok({
            eprintln!("WARNING: coateddiffuse has no coating");
            Arc::new(parse_dict2!(input, PbrtCoatedDiffuseMaterial;
                albedo, Either::A(Color::black()), [
                    "rgb albedo", Either::A(texture::parse_rgb(input)?);
                    "spectrum albedo", Either::A(texture::parse_spectrum(input)?);
                    "texture albedo", Either::B(parse_2d_spectrum_texture(input, context)?)
                ];
                g, Either::A(0.0), [
                    "float g", Either::A(input.parse_parameter()?);
                    "texture g", Either::B(parse_2d_float_texture(input, context)?)
                ];
                maxdepth, 10, ["integer maxdepth", input.parse_parameter()?];
                nsamples, 1, ["integer nsamples", input.parse_parameter()?];
                thickness, 0.01, ["float thickness", input.parse_parameter()?];
                roughness, Either::A(0.0), [
                    "float roughness", Either::A(input.parse_parameter()?);
                    "texture roughness", Either::B(parse_2d_float_texture(input, context)?)
                ];
                uroughness, Either::A(0.0), [
                    "float uroughness", Either::A(input.parse_parameter()?);
                    "texture uroughness", Either::B(parse_2d_float_texture(input, context)?)
                ];
                vroughness, Either::A(0.0), [
                    "float vroughness", Either::A(input.parse_parameter()?);
                    "texture vroughness", Either::B(parse_2d_float_texture(input, context)?)
                ];
                reflectance, Either::A(Color::black()), [
                    "rgb reflectance", Either::A(texture::parse_rgb(input)?);
                    "spectrum reflectance", Either::A(texture::parse_spectrum(input)?);
                    "texture reflectance", Either::B(parse_2d_spectrum_texture(input, context)?)
                ];
                eta, Either::A(Either::A(1.5)), [
                    "float eta", Either::A(Either::A(input.parse_parameter()?));
                    "rgb eta", Either::B(Either::A(parse_rgb(input)?));
                    "spectrum eta", Either::B(Either::A(texture::parse_spectrum(input)?));
                    "texture eta", {
                        let n = input.parse_parameter()?;
                        let t = context.get_texture(&n).ok_or_else(|| miette!("Unknown texture"))?;
                        match Arc::clone(t).get_2d_spectrum_texture() {
                            Ok(t) => Either::B(Either::B(t)),
                            Err(_) => Either::A(Either::B(Arc::clone(t).get_2d_float_texture()?))
                         }
                    }
                ]
            ))
        }),
        "dielectric" => Ok(Arc::new(parse_dict2!(input, PbrtDielectricMaterial;
            eta, Either::A(Either::A(1.5)), [
                "float eta", Either::A(Either::A(input.parse_parameter()?));
                "rgb eta", Either::B(Either::A(parse_rgb(input)?));
                "spectrum eta", Either::B(Either::A(texture::parse_spectrum(input)?));
                "texture eta", {
                    let n = input.parse_parameter()?;
                    let t = context.get_texture(&n).ok_or_else(|| miette!("Unknown texture"))?;
                    match Arc::clone(t).get_2d_spectrum_texture() {
                        Ok(t) => Either::B(Either::B(t)),
                        Err(_) => Either::A(Either::B(Arc::clone(t).get_2d_float_texture()?))
                     }
                }
            ];
            roughness, Either::A(0.0), [
                "float roughness", Either::A(input.parse_parameter()?);
                "texture roughness", Either::B(parse_2d_float_texture(input, context)?)
            ];
            uroughness, Either::A(0.0), [
                "float uroughness", Either::A(input.parse_parameter()?);
                "texture uroughness", Either::B(parse_2d_float_texture(input, context)?)
            ];
            vroughness, Either::A(0.0), [
                "float vroughness", Either::A(input.parse_parameter()?);
                "texture vroughness", Either::B(parse_2d_float_texture(input, context)?)
            ]
        ))),
        _ => Err(miette!("Unknown material {material}")),
    }
}
