use std::path::Path;

use crate::core::image::{Image, ImageIO};
use crate::core::light::{CreateLight, lookup_spectrum};
use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture;
use crate::utils::{Arena, FileLoc, ParameterDictionary, Upload, resolve_filename};
use anyhow::{Result, anyhow};
use shared::core::geometry::Point2i;
use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::{Medium, MediumInterface};
use shared::core::shape::{Shape, ShapeTrait};
use shared::core::spectrum::Spectrum;
use shared::core::texture::GPUFloatTexture;
use shared::core::texture::{SpectrumType, TextureEvalContext};
use shared::lights::DiffuseAreaLight;
use shared::spectra::RGBColorSpace;
use shared::utils::{Ptr, Transform};
use shared::{Float, PI};

pub trait CreateDiffuseLight {
    fn new(
        render_from_light: Transform,
        medium_interface: MediumInterface,
        le: Spectrum,
        scale: Float,
        shape: Ptr<Shape>,
        alpha: Ptr<GPUFloatTexture>,
        image: Ptr<Image>,
        colorspace: Ptr<RGBColorSpace>,
        two_sided: bool,
    ) -> Self;
}

impl CreateDiffuseLight for DiffuseAreaLight {
    fn new(
        render_from_light: Transform,
        medium_interface: MediumInterface,
        le: Spectrum,
        scale: Float,
        shape: Ptr<Shape>,
        alpha: Ptr<GPUFloatTexture>,
        image: Ptr<Image>,
        colorspace: Ptr<RGBColorSpace>,
        two_sided: bool,
    ) -> Self {
        let is_constant_zero = match &*alpha {
            GPUFloatTexture::Constant(tex) => tex.evaluate(&TextureEvalContext::default()) == 0.0,
            _ => false,
        };

        let (light_type, stored_alpha) = if is_constant_zero {
            (LightType::DeltaPosition, None)
        } else {
            (LightType::Area, Some(alpha))
        };

        let base = LightBase::new(light_type, render_from_light, medium_interface);

        let lemit = lookup_spectrum(&le);

        if !image.is_null() {
            let desc = image
                .get_channel_desc(&["R", "G", "B"])
                .expect("Image used for DiffuseAreaLight doesn't have R, G, B channels");

            assert_eq!(3, desc.size(), "Image channel description size mismatch");
            assert!(
                desc.is_identity(),
                "Image channel description is not identity"
            );

            assert!(
                !colorspace.is_null(),
                "Image provided but ColorSpace is missing"
            );
        }

        let is_triangle_or_bilinear =
            matches!(*shape, Shape::Triangle(_) | Shape::BilinearPatch(_));
        if render_from_light.has_scale(None) && !is_triangle_or_bilinear {
            println!(
                "Scaling detected in rendering to light space transformation! \
                  The system has numerous assumptions, implicit and explicit, \
                  that this transform will have no scale factors in it. \
                  Proceed at your own risk; your image may have errors."
            );
        }

        Self {
            base,
            area: shape.area(),
            image: Ptr::from(image.device()),
            colorspace,
            shape,
            alpha: stored_alpha.expect("Could not retrieve texture"),
            lemit: Ptr::from(&lemit.device()),
            two_sided,
            scale,
        }
    }
}

impl CreateLight for DiffuseAreaLight {
    fn create(
        render_from_light: Transform,
        medium: Medium,
        params: &ParameterDictionary,
        loc: &FileLoc,
        shape: &Shape,
        alpha: &FloatTexture,
        colorspace: Option<&RGBColorSpace>,
        arena: &Arena,
    ) -> Result<Light> {
        let mut l = params.get_one_spectrum("l", None, SpectrumType::Illuminant);
        let illum_spec = Spectrum::Dense(colorspace.unwrap().illuminant);
        let mut scale = params.get_one_float("scale", 1.)?;
        let two_sided = params.get_one_bool("twosided", false)?;

        let filename = resolve_filename(&params.get_one_string("filename", "")?);
        let (image, image_color_space) = if !filename.is_empty() {
            if l.is_some() {
                return Err(anyhow!("{}: both \"L\" and \"filename\" specified", loc));
            }

            let im = Image::read(Path::new(&filename), None)?;

            if im.image.has_any_infinite_pixels() {
                return Err(anyhow!("{}: image has infinite pixel values", loc));
            }
            if im.image.has_any_nan_pixels() {
                return Err(anyhow!("{}: image has NaN pixel values", loc));
            }

            let channel_desc = im
                .image
                .get_channel_desc(&["R", "G", "B"])
                .map_err(|_| anyhow!("{}: image must have R, G, B channels", loc))?;

            let image = im.image.select_channels(&channel_desc);
            let cs = im.metadata.get_colorspace();

            (Some(image), cs)
        } else {
            if l.is_none() {
                l = Some(illum_spec);
            }
            (None, None)
        };

        let l_for_scale = l.as_ref().unwrap_or(&illum_spec);
        scale /= spectrum_to_photometric(*l_for_scale);

        let phi_v = params.get_one_float("power", -1.0)?;
        if phi_v > 0.0 {
            // k_e is the emissive power of the light as defined by the spectral
            // distribution and texture and is used to normalize the emitted
            // radiance such that the user-defined power will be the actual power
            // emitted by the light.

            let mut k_e: Float = 1.0;

            if let Some(ref img) = image {
                // Get the appropriate luminance vector from the image colour space
                let lum_vec = image_color_space.unwrap().luminance_vector();

                let mut sum_k_e = 0.0;
                let res = img.resolution();

                for y in 0..res.y() {
                    for x in 0..res.x() {
                        let r = img.get_channel(Point2i::new(x, y), 0);
                        let g = img.get_channel(Point2i::new(x, y), 1);
                        let b = img.get_channel(Point2i::new(x, y), 2);

                        sum_k_e += r * lum_vec[0] + g * lum_vec[1] + b * lum_vec[2];
                    }
                }
                k_e = sum_k_e / (res.x() * res.y()) as Float;
            }

            let side_factor = if two_sided { 2.0 } else { 1.0 };
            k_e *= side_factor * shape.area() * PI;

            // now multiply up scale to hit the target power
            scale *= phi_v / k_e;
        }

        let specific = DiffuseAreaLight::new(
            render_from_light,
            medium.into(),
            *l_for_scale,
            scale,
            shape.upload(arena),
            alpha.upload(arena),
            Ptr::from(&image.unwrap()),
            image_color_space.upload(arena),
            true,
        );

        Ok(Light::DiffuseArea(specific))
    }
}
