use crate::core::image::{HostImage, ImageIO};
use crate::core::light::lookup_spectrum;
use crate::core::spectrum::spectrum_to_photometric;
use crate::spectra::get_spectra_context;
use crate::utils::resolve_filename;
use crate::utils::sampling::{PiecewiseConstant2D, WindowedPiecewiseConstant2D};
use crate::{Arena, FileLoc, ParameterDictionary, ArenaUpload, Upload};
use anyhow::{anyhow, Result};
use rayon::prelude::*;
use shared::core::camera::CameraTransform;
use shared::core::geometry::{cos_theta, Bounds2f, Frame, Point2f, Point2i, Point3f, VectorLike};
use shared::core::image::WrapMode;
use shared::core::light::{Light, LightBase, LightType};
use shared::core::medium::{Medium, MediumInterface};
use shared::core::spectrum::Spectrum;
use shared::core::texture::SpectrumType;
use shared::lights::{ImageInfiniteLight, PortalInfiniteLight, UniformInfiniteLight};
use shared::spectra::{DenselySampledSpectrum, RGBColorSpace};
use shared::utils::math::{equal_area_sphere_to_square, equal_area_square_to_sphere};
use shared::utils::sampling::{PiecewiseConstant2D, WindowedPiecewiseConstant2D};
use shared::{Float, Ptr, Transform, PI};
use std::path::Path;

pub fn create(
    render_from_light: Transform,
    _medium: Option<Medium>,
    camera_transform: CameraTransform,
    parameters: &ParameterDictionary,
    colorspace: Option<&RGBColorSpace>,
    loc: &FileLoc,
    arena: &Arena,
) -> Result<Light> {
    let default_cs = crate::spectra::default_colorspace();
    let cs = colorspace.unwrap_or(&default_cs);
    let l = parameters.get_spectrum_array("L", SpectrumType::Illuminant);
    let mut scale = parameters.get_one_float("scale", 1.0)?;
    let portal = parameters.get_point3f_array("portal")?;
    let filename = resolve_filename(&parameters.get_one_string("filename", "")?);
    let e_v = parameters.get_one_float("illuminance", -1.0)?;

    let has_spectrum = !l.is_empty();
    let has_file = !filename.is_empty();
    let has_portal = !portal.is_empty();

    if has_spectrum && has_file {
        return Err(anyhow!(
            "{}: cannot specify both \"L\" and \"filename\"",
            loc
        ));
    }

    // Uniform infinite light (no image)
    if !has_file && !has_portal {
        let spectrum = if has_spectrum {
            scale /= spectrum_to_photometric(l[0]);
            l[0]
        } else {
            Spectrum::Dense(cs.illuminant)
        };

        if e_v > 0.0 {
            scale *= e_v / PI;
        }

        let lemit = lookup_spectrum(&spectrum);
        let light = UniformInfiniteLight::new(render_from_light, scale, arena.alloc(*lemit));
        return Ok(Light::InfiniteUniform(light));
    }

    // Image-based lights
    let (image, image_cs) = load_image(&filename, &l, cs, loc)?;

    scale /= spectrum_to_photometric(Spectrum::Dense(image_cs.illuminant));

    if e_v > 0.0 {
        let k_e = compute_hemisphere_illuminance(&image, &image_cs);
        scale *= e_v / k_e;
    }

    if has_portal {
        create_portal_light(
            render_from_light,
            scale,
            image,
            image_cs,
            &portal,
            camera_transform,
            loc,
            arena,
        )
    } else {
        create_image_light(render_from_light, scale, image, image_cs, arena)
    }
}

fn create_image_light(
    render_from_light: Transform,
    scale: Float,
    image: Image,
    image_cs: RGBColorSpace,
    arena: &Arena,
) -> Result<Light> {
    let res = image.resolution();
    assert_eq!(
        res.x(),
        res.y(),
        "Image must be square for equal-area mapping"
    );

    let (n_u, n_v) = (res.x() as usize, res.y() as usize);

    // Extract luminance data
    let image_ptr = image.upload(arena);
    let value = &image;
    let mut data: Vec<Float> = (0..n_v)
        .flat_map(|v| {
            (0..n_u).map(move |u| {
                value
                    .get_channels(Point2i::new(u as i32, v as i32))
                    .average()
            })
        })
        .collect();

    let distrib = PiecewiseConstant2D::from_slice(&data, n_u, n_v, Bounds2f::unit());

    // Build compensated distribution
    let average = data.iter().sum::<Float>() / data.len() as Float;
    let mut all_zero = true;
    for v in &mut data {
        *v = (*v - average).max(0.0);
        all_zero &= *v == 0.0;
    }
    if all_zero {
        data.fill(1.0);
    }
    let compensated_distrib = PiecewiseConstant2D::from_slice(&data, n_u, n_v, Bounds2f::unit());

    let light = ImageInfiniteLight::new(
        render_from_light,
        scale,
        image_ptr,
        arena.alloc(image_cs),
        arena.alloc(distrib),
        arena.alloc(compensated_distrib),
    );

    Ok(Light::InfiniteImage(light))
}

fn create_portal_light(
    render_from_light: Transform,
    scale: Float,
    image: Image,
    image_cs: RGBColorSpace,
    portal_points: &[Point3f],
    camera_transform: CameraTransform,
    loc: &FileLoc,
    arena: &Arena,
) -> Result<Light> {
    let res = image.resolution();
    if res.x() != res.y() {
        return Err(anyhow!("{}: Portal light image must be square", loc));
    }

    // Validate portal
    if portal_points.len() != 4 {
        return Err(anyhow!(
            "{}: Portal requires exactly 4 vertices, got {}",
            loc,
            portal_points.len()
        ));
    }

    let portal: [Point3f; 4] = portal_points
        .iter()
        .map(|p| camera_transform.camera_from_world(0.0).apply_to_point(*p))
        .collect::<Vec<_>>()
        .try_into()
        .unwrap();

    let portal_frame = validate_and_build_portal_frame(&portal, loc)?;

    // Remap image through portal
    let remapped = remap_image_through_portal(&image, &render_from_light, &portal_frame);

    // Build distribution
    let duv_dw = |p: Point2f| -> Float {
        let (_, jacobian) = PortalInfiniteLight::render_from_image(portal_frame, p);
        jacobian
    };
    let d = remapped.get_sampling_distribution(
        duv_dw,
        Bounds2f::from_points(Point2f::zero(), Point2f::fill(1.)),
    );
    let distribution = WindowedPiecewiseConstant2D::new(d);

    let light = PortalInfiniteLight::new(
        render_from_light,
        scale,
        arena.alloc(remapped),
        arena.alloc(image_cs),
        portal,
        portal_frame,
        arena.alloc(distribution),
    );

    Ok(Light::InfinitePortal(light))
}

fn validate_and_build_portal_frame(portal: &[Point3f; 4], loc: &FileLoc) -> Result<Frame> {
    let p01 = (portal[1] - portal[0]).normalize();
    let p12 = (portal[2] - portal[1]).normalize();
    let p32 = (portal[2] - portal[3]).normalize();
    let p03 = (portal[3] - portal[0]).normalize();

    if (p01.dot(p32) - 1.0).abs() > 0.001 || (p12.dot(p03) - 1.0).abs() > 0.001 {
        return Err(anyhow!("{}: Portal edges not parallel", loc));
    }

    if p01.dot(p12).abs() > 0.001
        || p12.dot(p32).abs() > 0.001
        || p32.dot(p03).abs() > 0.001
        || p03.dot(p01).abs() > 0.001
    {
        return Err(anyhow!("{}: Portal edges not perpendicular", loc));
    }

    Ok(Frame::from_xy(p03, p01))
}

fn remap_image_through_portal(
    image: &Image,
    render_from_light: &Transform,
    portal_frame: &Frame,
) -> Image {
    let res = image.resolution();
    let (width, height) = (res.x() as usize, res.y() as usize);

    let mut pixels = vec![0.0f32; width * height * 3];

    pixels
        .par_chunks_mut(width * 3)
        .enumerate()
        .for_each(|(y, row)| {
            for x in 0..width {
                let uv = Point2f::new(
                    (x as Float + 0.5) / width as Float,
                    (y as Float + 0.5) / height as Float,
                );

                let (w_world, _) = PortalInfiniteLight::render_from_image(*portal_frame, uv);
                let w_local = render_from_light.apply_inverse_vector(w_world).normalize();
                let uv_equi = equal_area_sphere_to_square(w_local);

                for c in 0..3 {
                    row[x * 3 + c] = image.bilerp_channel_with_wrap(
                        uv_equi,
                        c as i32,
                        WrapMode::OctahedralSphere.into(),
                    );
                }
            }
        });

    Image::from_f32(pixels, res, &["R", "G", "B"])
}

fn load_image(
    filename: &str,
    l: &[Spectrum],
    colorspace: &RGBColorSpace,
    loc: &FileLoc,
) -> Result<(Image, RGBColorSpace)> {
    if filename.is_empty() {
        let stdspec = get_spectra_context();
        let rgb = l[0].to_rgb(colorspace, &stdspec);
        let image =
            Image::new_constant(Point2i::new(1, 1), &["R", "G", "B"], &[rgb.r, rgb.g, rgb.b]);
        return Ok((image, colorspace.clone()));
    }

    let im = Image::read(Path::new(filename), None)
        .map_err(|e| anyhow!("failed to load '{}': {}", filename, e))?;

    if im.image.has_any_infinite_pixels() || im.image.has_any_nan_pixels() {
        return Err(anyhow!("{}: image '{}' has invalid pixels", loc, filename));
    }

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

    let cs = im.metadata.colorspace.unwrap_or_else(|| colorspace.clone());
    Ok((im.image.select_channels(&desc), cs))
}

fn compute_hemisphere_illuminance(image: &Image, cs: &RGBColorSpace) -> Float {
    let lum = cs.luminance_vector();
    let res = image.resolution();

    let sum: Float = (0..res.y())
        .flat_map(|y| (0..res.x()).map(move |x| (x, y)))
        .filter_map(|(x, y)| {
            let u = (x as Float + 0.5) / res.x() as Float;
            let v = (y as Float + 0.5) / res.y() as Float;
            let w = equal_area_square_to_sphere(Point2f::new(u, v));

            if w.z() <= 0.0 {
                return None;
            }

            let p = Point2i::new(x, y);
            let r = image.get_channel(p, 0);
            let g = image.get_channel(p, 1);
            let b = image.get_channel(p, 2);

            Some((r * lum[0] + g * lum[1] + b * lum[2]) * cos_theta(w))
        })
        .sum();

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