use std::path::Path;

use crate::core::image::{Image, ImageIO};
use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture;
use crate::utils::resolve_filename;
use crate::utils::upload::ArenaUpload;
use crate::{Arena, FileLoc, ParameterDictionary};
use anyhow::{anyhow, Result};
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::{SpectrumType, TextureEvalContext};
use shared::lights::DiffuseAreaLight;
use shared::spectra::RGBColorSpace;
use shared::utils::Transform;
use shared::{Float, PI};

pub fn create(
    render_from_light: Transform,
    medium: Option<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 default_cs = crate::spectra::default_colorspace();
    let cs = colorspace.unwrap_or(&default_cs);
    let illum_spec = Spectrum::Dense(cs.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): (Option<Image>, Option<RGBColorSpace>) =
        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 {
        let mut k_e: Float = 1.0;

        if let Some(ref img) = image {
            let lum_vec = image_color_space
                .as_ref()
                .expect("image present but no color space")
                .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;
        scale *= phi_v / k_e;
    }

    // Upload alpha texture to GPU and check for constant-zero
    let alpha_ptr = arena.upload(alpha);
    let light_type = if alpha_ptr.is_constant_zero() {
        LightType::DeltaPosition
    } else {
        LightType::Area
    };

    let mi = match medium {
        Some(m) => {
            let ptr = arena.alloc(m);
            MediumInterface {
                inside: ptr,
                outside: ptr,
            }
        }
        None => MediumInterface::default(),
    };

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

    if let Some(ref img) = image {
        let desc = img
            .get_channel_desc(&["R", "G", "B"])
            .expect("Image used for DiffuseAreaLight doesn't have R, G, B channels");
        assert_eq!(3, desc.size());
        assert!(desc.is_identity());
        assert!(
            image_color_space.is_some(),
            "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 {
        eprintln!(
            "Warning: scaling detected in rendering-to-light transform; \
             image may have errors."
        );
    }

    let specific = DiffuseAreaLight {
        base,
        area: shape.area(),
        shape: arena.alloc(*shape),
        alpha: alpha_ptr,
        image: arena.alloc(image),
        colorspace: arena.alloc_opt(image_color_space),
        lemit: arena.alloc((*lookup_spectrum(l_for_scale)).clone()),
        two_sided,
        scale,
    };

    Ok(Light::DiffuseArea(specific))
}
