use crate::core::image::{Image, ImageIO};
use crate::core::spectrum::spectrum_to_photometric;
use crate::core::texture::FloatTexture;
use crate::utils::sampling::PiecewiseConstant2D;
use crate::utils::resolve_filename;
use crate::{Arena, FileLoc, ParameterDictionary};
use anyhow::{Result, anyhow};
use shared::Float;
use shared::core::geometry::{
    Bounds2f, Point2f, Point2i, Point3f, Vector3f, VectorLike, cos_theta,
};
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::lights::ProjectionLight;
use shared::spectra::RGBColorSpace;
use shared::utils::Transform;
use shared::utils::math::{radians, square};
use std::path::Path;

pub fn create(
    render_from_light: Transform,
    medium: Option<Medium>,
    parameters: &ParameterDictionary,
    loc: &FileLoc,
    _shape: &Shape,
    _alpha_text: &FloatTexture,
    _colorspace: Option<&RGBColorSpace>,
    arena: &Arena,
) -> Result<Light> {
    let mut scale = parameters.get_one_float("scale", 1.)?;
    let power = parameters.get_one_float("power", -1.)?;
    let fov = parameters.get_one_float("fov", 90.)?;

    let filename = resolve_filename(&parameters.get_one_string("filename", "")?);
    if filename.is_empty() {
        return Err(anyhow!(
            "{}: must provide filename for projection light",
            loc
        ));
    }

    let im = Image::read(Path::new(&filename), None)
        .map_err(|e| anyhow!("{}: could not load image '{}': {}", loc, filename, e))?;

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

    if im.image.has_any_nan_pixels() {
        return Err(anyhow!(
            "{}: image '{}' has NaN pixels, not suitable for light",
            loc,
            filename
        ));
    }

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

    let image = im.image.select_channels(&channel_desc);
    let colorspace = im
        .metadata
        .colorspace
        .ok_or_else(|| anyhow!("{}: image '{}' missing colorspace metadata", loc, filename))?;

    scale /= spectrum_to_photometric(Spectrum::Dense(colorspace.illuminant));
    if power > 0. {
        let k_e = compute_emissive_power(&image, &colorspace, fov);
        scale /= k_e;
    }

    let flip = Transform::scale(1., -1., 1.);
    let _render_from_light_flip = render_from_light * flip;

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

    let opposite = (radians(fov) / 2.0).tan();
    let res = image.resolution();
    let aspect = res.x() as Float / res.y() as Float;
    let aspect_ratio = if aspect > 1.0 { aspect } else { 1.0 / aspect };
    let a = 4.0 * square(opposite) * aspect_ratio;
    let screen_bounds = if aspect > 1.0 {
        Bounds2f::from_points(Point2f::new(-aspect, -1.0), Point2f::new(aspect, 1.0))
    } else {
        Bounds2f::from_points(
            Point2f::new(-1.0, -1.0 / aspect),
            Point2f::new(1.0, 1.0 / aspect),
        )
    };

    let hither = 1e-3;
    let screen_from_light = Transform::perspective(fov, hither, 1e30).unwrap();
    let light_from_screen = screen_from_light.inverse();

    let dwda = |p: Point2f| {
        let w = Vector3f::from(light_from_screen.apply_to_point(Point3f::new(p.x(), p.y(), 0.0)));
        cos_theta(w.normalize()).powi(3)
    };

    let d = image.get_sampling_distribution(dwda, screen_bounds);
    let distrib = PiecewiseConstant2D::from_slice(
        d.as_slice(),
        d.x_size() as usize,
        d.y_size() as usize,
        screen_bounds,
    );

    let specific = ProjectionLight {
        base,
        image: image.upload(arena),
        image_color_space: colorspace.upload(arena),
        distrib: distrib.upload(arena),
        screen_bounds,
        screen_from_light,
        light_from_screen,
        scale,
        hither,
        a,
    };

    Ok(Light::Projection(specific))
}

fn compute_screen_bounds(aspect: Float) -> Bounds2f {
    if aspect > 1.0 {
        Bounds2f::from_points(Point2f::new(-aspect, -1.0), Point2f::new(aspect, 1.0))
    } else {
        Bounds2f::from_points(
            Point2f::new(-1.0, -1.0 / aspect),
            Point2f::new(1.0, 1.0 / aspect),
        )
    }
}

fn compute_emissive_power(image: &Image, colorspace: &RGBColorSpace, fov: Float) -> Float {
    let res = image.resolution();
    let aspect = res.x() as Float / res.y() as Float;
    let screen_bounds = compute_screen_bounds(aspect);

    let hither = 1e-3;
    let screen_from_light =
        Transform::perspective(fov, hither, 1e30).expect("Failed to create perspective transform");
    let light_from_screen = screen_from_light.inverse();

    let opposite = (radians(fov) / 2.0).tan();
    let aspect_factor = if aspect > 1.0 { aspect } else { 1.0 / aspect };
    let a = 4.0 * square(opposite) * aspect_factor;

    let luminance = colorspace.luminance_vector();
    let mut sum: Float = 0.0;

    for y in 0..res.y() {
        for x in 0..res.x() {
            let lerp_factor = Point2f::new(
                (x as Float + 0.5) / res.x() as Float,
                (y as Float + 0.5) / res.y() as Float,
            );
            let ps = screen_bounds.lerp(lerp_factor);
            let w_point = light_from_screen.apply_to_point(Point3f::new(ps.x(), ps.y(), 0.0));
            let w = Vector3f::from(w_point).normalize();
            let dwda = w.z().powi(3);

            for c in 0..3 {
                sum += image.get_channel(Point2i::new(x, y), c) * luminance[c] * dwda;
            }
        }
    }

    a * sum / (res.x() * res.y()) as Float
}
