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::sampling::PiecewiseConstant2D;
use crate::utils::{Arena, FileLoc, ParameterDictionary, resolve_filename};
use log::error;
use shared::core::color::ColorEncoding;
use shared::core::geometry::Point2i;
use shared::core::image::PixelFormat;
use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::{Medium, MediumInterface};
use shared::core::shape::Shape;
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType;
use shared::lights::GoniometricLight;
use shared::spectra::RGBColorSpace;
use shared::utils::{Ptr, Transform};
use shared::{Float, PI};
use std::fmt::Error;

pub trait CreateGoniometricLight {
    fn new(
        render_from_light: Transform,
        medium_interface: MediumInterface,
        le: Spectrum,
        scale: Float,
        image: Ptr<Image>,
    ) -> Self;
}

impl CreateGoniometricLight for GoniometricLight {
    fn new(
        render_from_light: Transform,
        medium_interface: MediumInterface,
        le: Spectrum,
        scale: Float,
        image: Ptr<Image>,
    ) -> Self {
        let base = LightBase::new(
            LightType::DeltaPosition,
            render_from_light,
            medium_interface,
        );

        let iemit = lookup_spectrum(le);
        let d = image.unwrap().get_sampling_distribution_uniform();
        let distrib = PiecewiseConstant2D::new_with_data(d);
        Self {
            base,
            iemit,
            scale,
            image,
            distrib,
        }
    }
}

impl CreateLight for GoniometricLight {
    fn create(
        arena: &mut Arena,
        render_from_light: Transform,
        medium: Medium,
        params: &ParameterDictionary,
        loc: &FileLoc,
        shape: &Shape,
        alpha_text: &FloatTexture,
        colorspace: Option<&RGBColorSpace>,
    ) -> Result<Light, Error> {
        let i = params.get_one_spectrum(
            "I",
            colorspace.unwrap().illuminant,
            SpectrumType::Illuminant,
        );
        let mut scale = params.get_one_float("scale", 1.);
        let filename = resolve_filename(params.get_one_string("filename", ""));
        let mut image: Option<Image> = None;
        let image = if filename.is_empty() {
            None
        } else {
            let im = Image::read(&filename, None)
                .map_err(|e| error!(loc, "could not load image '{}': {}", filename, e))?;

            let loaded = im.image;
            let res = loaded.resolution();

            if loaded.has_any_infinite_pixels() {
                return Err(error!(
                    loc,
                    "image '{}' has infinite pixels, not suitable for light", filename
                ));
            }

            if res.x != res.y {
                return Err(error!(
                    loc,
                    "image resolution ({}, {}) is non-square; unlikely to be an equal-area map",
                    res.x,
                    res.y
                ));
            }

            Some(convert_to_luminance_image(&loaded, &filename, loc)?)
        };

        scale /= spectrum_to_photometric(&i);
        let phi_v = params.get_one_float("power", -1.0);

        if phi_v > 0.0 {
            if let Some(ref img) = image {
                let k_e = compute_emissive_power(image);
                scale *= phi_v / k_e;
            }
        }

        let swap_yz: [Float; 16] = [
            1., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1.,
        ];
        let t = Transform::from_flat(swap_yz);
        let final_render_from_light = render_from_light * t;

        let specific =
            GoniometricLight::new(final_render_from_light, medium.into(), i, scale, image);

        Ok(Light::Goniometric(specific))
    }
}

fn convert_to_luminance_image(
    image: &Image,
    filename: &str,
    loc: &FileLoc,
) -> Result<Image, Error> {
    let res = image.resolution();
    let rgb_desc = image.get_channel_desc(&["R", "G", "B"]);
    let y_desc = image.get_channel_desc(&["Y"]);

    match (rgb_desc, y_desc) {
        (Ok(_), Ok(_)) => Err(error!(
            loc,
            "image '{}' has both RGB and Y channels; ambiguous", filename
        )),

        (Ok(_), Err(_)) => {
            // Convert RGB to Y (luminance)
            let mut y_pixels = Vec::with_capacity((res.x * res.y) as usize);

            for y in 0..res.y {
                for x in 0..res.x {
                    let r = image.get_channel(Point2i::new(x, y), 0);
                    let g = image.get_channel(Point2i::new(x, y), 1);
                    let b = image.get_channel(Point2i::new(x, y), 2);
                    y_pixels.push((r + g + b) / 3.0);
                }
            }

            Ok(Image::from_pixels(
                PixelFormat::F32,
                res,
                &["Y"],
                ColorEncoding::Linear,
                &y_pixels,
            ))
        }

        (Err(_), Ok(_)) => {
            // Already has Y channel, use as-is
            Ok(image.clone())
        }

        (Err(_), Err(_)) => Err(error!(
            loc,
            "image '{}' has neither RGB nor Y channels", filename
        )),
    }
}

fn compute_emissive_power(image: &Image) -> Float {
    let res = image.resolution();
    let mut sum_y = 0.0;

    for y in 0..res.y {
        for x in 0..res.x {
            sum_y += image.get_channel(Point2i::new(x, y), 0);
        }
    }

    4.0 * PI * sum_y / (res.x * res.y) as Float
}
