use crate::core::image::ImageMetadata;
use crate::core::image::{Image, ImageIO};
use crate::globals::get_options;
use crate::utils::read_float_file;
use crate::{Arena, FileLoc, ParameterDictionary};
use anyhow::{anyhow, Result};
use shared::cameras::*;
use shared::core::camera::{Camera, CameraBase, CameraTrait, CameraTransform};
use shared::core::color::SRGB;
use shared::core::film::Film;
use shared::core::geometry::{Bounds2f, Point2f, Point2i, Vector2f, Vector3f};
use shared::core::image::PixelFormat;
use shared::core::medium::Medium;
use shared::utils::math::square;
use shared::{Float, Ptr, PI};
use std::path::Path;
use std::sync::Arc;

#[repr(C)]
#[derive(Clone, Debug)]
pub struct CameraBaseParameters {
    pub camera_transform: CameraTransform,
    pub shutter_open: Float,
    pub shutter_close: Float,
    pub film: Arc<Film>,
    pub medium: Option<Arc<Medium>>,
}

impl CameraBaseParameters {
    pub fn new(
        camera_transform: &CameraTransform,
        film: Arc<Film>,
        medium: Option<Arc<Medium>>,
        params: &ParameterDictionary,
        loc: &FileLoc,
    ) -> Result<Self> {
        let mut shutter_open = params.get_one_float("shutteropen", 0.)?;
        let mut shutter_close = params.get_one_float("shutterclose", 1.)?;
        if shutter_close < shutter_open {
            eprint!(
                "{}: Shutter close time {} < shutter open {}. Swapping",
                loc, shutter_close, shutter_open
            );
            std::mem::swap(&mut shutter_open, &mut shutter_close);
        }

        Ok(CameraBaseParameters {
            camera_transform: camera_transform.clone(),
            shutter_open,
            shutter_close,
            film,
            medium,
        })
    }
}

pub trait CameraBaseFactory {
    fn create(p: CameraBaseParameters) -> CameraBase {
        CameraBase {
            camera_transform: p.camera_transform.clone(),
            shutter_open: p.shutter_open,
            shutter_close: p.shutter_close,
            film: Ptr::from(p.film.clone().as_ref()),
            medium: match p.medium {
                Some(ref m) => Ptr::from(m.as_ref()),
                None => Ptr::null(),
            },
            min_pos_differential_x: Vector3f::default(),
            min_pos_differential_y: Vector3f::default(),
            min_dir_differential_x: Vector3f::default(),
            min_dir_differential_y: Vector3f::default(),
        }
    }
}

impl CameraBaseFactory for CameraBase {}

pub trait InitMetadata {
    fn init_metadata(&self, metadata: &mut ImageMetadata);
}

impl InitMetadata for CameraBase {
    fn init_metadata(&self, metadata: &mut ImageMetadata) {
        let camera_from_world = self.camera_transform.camera_from_world(self.shutter_open);
        metadata.camera_from_world = Some(camera_from_world.get_matrix());
    }
}

impl InitMetadata for Camera {
    fn init_metadata(&self, metadata: &mut ImageMetadata) {
        self.base().init_metadata(metadata);
    }
}

pub trait CameraFactory {
    fn create(
        name: &str,
        params: &ParameterDictionary,
        camera_transform: &CameraTransform,
        medium: Option<Arc<Medium>>,
        film: Arc<Film>,
        loc: &FileLoc,
        arena: &Arena,
    ) -> Result<Self>
    where
        Self: Sized;
}

impl CameraFactory for Camera {
    fn create(
        name: &str,
        params: &ParameterDictionary,
        camera_transform: &CameraTransform,
        medium: Option<Arc<Medium>>,
        film: Arc<Film>,
        loc: &FileLoc,
        arena: &Arena,
    ) -> Result<Self>
    where
        Self: Sized,
    {
        match name {
            "perspective" => {
                let full_res = film.full_resolution();
                let camera_params =
                    CameraBaseParameters::new(camera_transform, film, medium, params, loc)?;
                let base = CameraBase::create(camera_params);
                let lens_radius = params.get_one_float("lensradius", 0.)?;
                let focal_distance = params.get_one_float("focaldistance", 1e6)?;
                let frame = params.get_one_float(
                    "frameaspectratio",
                    full_res.x() as Float / full_res.y() as Float,
                )?;

                let mut screen = if frame > 1. {
                    Bounds2f::from_points(Point2f::new(-frame, -1.), Point2f::new(frame, 1.))
                } else {
                    Bounds2f::from_points(
                        Point2f::new(-1., -1. / frame),
                        Point2f::new(1., 1. / frame),
                    )
                };

                let sw = params.get_float_array("screenwindow")?;
                if !sw.is_empty() {
                    if get_options().fullscreen {
                        eprint!("Screenwindow is ignored in fullscreen mode");
                    } else {
                        if sw.len() == 4 {
                            screen = Bounds2f::from_points(
                                Point2f::new(sw[0], sw[2]),
                                Point2f::new(sw[1], sw[3]),
                            );
                        } else {
                            return Err(anyhow!(
                                "{}: screenwindow param must have four values",
                                loc
                            ));
                        }
                    }
                }

                let fov = params.get_one_float("fov", 90.)?;
                let camera = PerspectiveCamera::new(base, fov, screen, lens_radius, focal_distance);
                arena.alloc(camera);

                Ok(Camera::Perspective(camera))
            }

            "orthographic" => {
                let full_res = film.full_resolution();
                let camera_params =
                    CameraBaseParameters::new(camera_transform, film, medium, params, loc)?;
                let base = CameraBase::create(camera_params);
                let lens_radius = params.get_one_float("lensradius", 0.)?;
                let focal_distance = params.get_one_float("focaldistance", 1e6)?;
                let frame = params.get_one_float(
                    "frameaspectratio",
                    full_res.x() as Float / full_res.y() as Float,
                )?;

                let mut screen = if frame > 1. {
                    Bounds2f::from_points(Point2f::new(-frame, -1.), Point2f::new(frame, 1.))
                } else {
                    Bounds2f::from_points(
                        Point2f::new(-1., -1. / frame),
                        Point2f::new(1., 1. / frame),
                    )
                };

                let sw = params.get_float_array("screenwindow")?;
                if !sw.is_empty() {
                    if get_options().fullscreen {
                        eprint!("Screenwindow is ignored in fullscreen mode");
                    } else {
                        if sw.len() == 4 {
                            screen = Bounds2f::from_points(
                                Point2f::new(sw[0], sw[2]),
                                Point2f::new(sw[1], sw[3]),
                            );
                        } else {
                            return Err(anyhow!(
                                "{}: screenwindow param must have four values",
                                loc
                            ));
                        }
                    }
                }
                let camera = OrthographicCamera::new(base, screen, lens_radius, focal_distance);
                arena.alloc(camera);
                Ok(Camera::Orthographic(camera))
            }
            "realistic" => {
                let camera_params =
                    CameraBaseParameters::new(camera_transform, film, medium, params, loc)?;
                let base = CameraBase::create(camera_params);
                let aperture_diameter = params.get_one_float("aperturediameter", 1.)?;
                let focal_distance = params.get_one_float("focaldistance", 10.)?;
                let lens_file = params.get_one_string("lensfile", "")?;

                if lens_file.is_empty() {
                    return Err(anyhow!("{}: No lens file supplied", loc));
                }

                let lens_params = read_float_file(lens_file.as_str()).map_err(|e| anyhow!(e))?;
                if lens_params.len() % 4 != 0 {
                    return Err(anyhow!(
                        "{}: excess values in lens specification file; must be multiple-of-four values, read  {}",
                        loc,
                        lens_params.len()
                    ));
                }

                let builtin_res = 256;
                let rasterize = |vert: &[Point2f]| -> Image {
                    let mut image = Image::new(
                        PixelFormat::F32,
                        Point2i::new(builtin_res, builtin_res),
                        &["Y"],
                        SRGB.into(),
                    );

                    let res = image.resolution();
                    for y in 0..res.y() {
                        for x in 0..res.x() {
                            // Map pixel to [-1, 1] range
                            let p = Point2f::new(
                                -1.0 + 2.0 * (x as Float + 0.5) / res.x() as Float,
                                -1.0 + 2.0 * (y as Float + 0.5) / res.y() as Float,
                            );

                            let mut winding_number = 0;
                            // Winding number test against edges
                            for i in 0..vert.len() {
                                let i1 = (i + 1) % vert.len();
                                let v_i = vert[i];
                                let v_i1 = vert[i1];

                                let e = (p.x() - v_i.x()) * (v_i1.y() - v_i.y())
                                    - (p.y() - v_i.y()) * (v_i1.x() - v_i.x());

                                if v_i.y() <= p.y() {
                                    if v_i1.y() > p.y() && e > 0.0 {
                                        winding_number += 1;
                                    }
                                } else if v_i1.y() <= p.y() && e < 0.0 {
                                    winding_number -= 1;
                                }
                            }

                            image.set_channel(
                                Point2i::new(x, y),
                                0,
                                if winding_number == 0 { 0.0 } else { 1.0 },
                            );
                        }
                    }
                    image
                };

                let aperture_name = params.get_one_string("aperture", "")?;
                let mut aperture_image: Option<Image> = None;

                if !aperture_name.is_empty() {
                    match aperture_name.as_str() {
                        "gaussian" => {
                            let mut img = Image::new(
                                PixelFormat::F32,
                                Point2i::new(builtin_res, builtin_res),
                                &["Y"],
                                SRGB.into(),
                            );
                            let res = img.resolution();
                            for y in 0..res.y() {
                                for x in 0..res.x() {
                                    let uv = Point2f::new(
                                        -1.0 + 2.0 * (x as Float + 0.5) / res.x() as Float,
                                        -1.0 + 2.0 * (y as Float + 0.5) / res.y() as Float,
                                    );
                                    let r2 = square(uv.x()) + square(uv.y());
                                    let sigma2 = 1.0;
                                    let v = ((-r2 / sigma2).exp() - (-1.0 / sigma2).exp()).max(0.0);
                                    img.set_channel(Point2i::new(x, y), 0, v);
                                }
                            }
                            aperture_image = Some(img);
                        }
                        "square" => {
                            let mut img = Image::new(
                                PixelFormat::F32,
                                Point2i::new(builtin_res, builtin_res),
                                &["Y"],
                                SRGB.into(),
                            );
                            let low = (0.25 * builtin_res as Float) as i32;
                            let high = (0.75 * builtin_res as Float) as i32;
                            for y in low..high {
                                for x in low..high {
                                    img.set_channel(Point2i::new(x, y), 0, 4.0);
                                }
                            }
                            aperture_image = Some(img);
                        }
                        "pentagon" => {
                            let c1 = (5.0f32.sqrt() - 1.0) / 4.0;
                            let c2 = (5.0f32.sqrt() + 1.0) / 4.0;
                            let s1 = (10.0 + 2.0 * 5.0f32.sqrt()).sqrt() / 4.0;
                            let s2 = (10.0 - 2.0 * 5.0f32.sqrt()).sqrt() / 4.0;
                            let mut vert = [
                                Point2f::new(0.0, 1.0),
                                Point2f::new(s1, c1),
                                Point2f::new(s2, -c2),
                                Point2f::new(-s2, -c2),
                                Point2f::new(-s1, c1),
                            ];
                            for v in vert.iter_mut() {
                                *v = Point2f::from(Vector2f::from(*v) * 0.8);
                            }
                            aperture_image = Some(rasterize(&vert));
                        }
                        "star" => {
                            let mut vert = Vec::with_capacity(10);
                            for i in 0..10 {
                                let r = if i % 2 == 1 {
                                    1.0
                                } else {
                                    (72.0f32.to_radians().cos()) / (36.0f32.to_radians().cos())
                                };
                                let angle = PI * i as Float / 5.0;
                                vert.push(Point2f::new(r * angle.cos(), r * angle.sin()));
                            }
                            vert.reverse();
                            aperture_image = Some(rasterize(&vert));
                        }
                        _ => {
                            if let Ok(im) = Image::read(Path::new(&aperture_name), None) {
                                if im.image.n_channels() > 1 {
                                    let mut mono = Image::new(
                                        PixelFormat::F32,
                                        im.image.resolution(),
                                        &["Y"],
                                        SRGB.into(),
                                    );
                                    let res = mono.resolution();
                                    for y in 0..res.y() {
                                        for x in 0..res.x() {
                                            let avg =
                                                im.image.get_channels(Point2i::new(x, y)).average();
                                            mono.set_channel(Point2i::new(x, y), 0, avg);
                                        }
                                    }
                                    aperture_image = Some(mono);
                                } else {
                                    aperture_image = Some(im.image);
                                }
                            }
                        }
                    }
                }

                let camera = RealisticCamera::new(
                    base,
                    &lens_params,
                    focal_distance,
                    aperture_diameter,
                    Ptr::from(&*aperture_image.unwrap()),
                );

                arena.alloc(camera);
                Ok(Camera::Realistic(camera))
            }
            "spherical" => {
                let _full_res = film.full_resolution();
                let camera_params =
                    CameraBaseParameters::new(camera_transform, film, medium, params, loc)?;
                let base = CameraBase::create(camera_params);
                let m = params.get_one_string("mapping", "equalarea")?;
                let mapping = match m.as_str() {
                    "equalarea" => Mapping::EqualArea,
                    "equirectangular" => Mapping::EquiRectangular,
                    _ => {
                        return Err(anyhow!(
                            "{}: unknown mapping for spherical camera at {}",
                            m,
                            loc
                        ));
                    }
                };

                let camera = SphericalCamera { mapping, base };

                arena.alloc(camera);
                Ok(Camera::Spherical(camera))
            }
            _ => Err(anyhow!("Camera type '{}' unknown at {}", name, loc)),
        }
    }
}
