use super::{Normal3f, Point3f, Point3fi, Vector3f, VectorLike};
use crate::core::medium::Medium;
use crate::core::pbrt::Float;
use crate::utils::math::{next_float_down, next_float_up};
use crate::utils::ptr::Ptr;

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct Ray {
    pub o: Point3f,
    pub d: Vector3f,
    pub time: Float,
    pub medium: Ptr<Medium>,
    // We do this instead of creating a trait for Rayable or some gnarly thing like that
    pub has_differentials: bool,
    pub differential: RayDifferential,
}

impl Default for Ray {
    fn default() -> Self {
        Self {
            o: Point3f::new(0.0, 0.0, 0.0),
            d: Vector3f::new(0.0, 0.0, 0.0),
            medium: Ptr::null(),
            time: 0.0,
            has_differentials: false,
            differential: RayDifferential::default(),
        }
    }
}

impl Ray {
    pub fn new(o: Point3f, d: Vector3f, time: Option<Float>, medium: &Medium) -> Self {
        Self {
            o,
            d,
            time: time.unwrap_or_else(|| Self::default().time),
            medium: Ptr::from(medium),
            ..Self::default()
        }
    }

    pub fn at(&self, t: Float) -> Point3f {
        self.o + self.d * t
    }

    pub fn offset_origin(p: &Point3fi, n: &Normal3f, w: &Vector3f) -> Point3f {
        let d: Float = Vector3f::from(n.abs()).dot(p.error());
        let normal: Vector3f = Vector3f::from(*n);

        let mut offset = p.midpoint();
        if w.dot(normal) < 0.0 {
            offset -= normal * d;
        } else {
            offset += normal * d;
        }

        for i in 0..3 {
            if n[i] > 0.0 {
                offset[i] = next_float_up(offset[i]);
            } else if n[i] < 0.0 {
                offset[i] = next_float_down(offset[i]);
            }
        }
        offset
    }

    pub fn spawn(pi: &Point3fi, n: &Normal3f, time: Float, d: Vector3f) -> Ray {
        let origin = Self::offset_origin(pi, n, &d);
        Ray {
            o: origin,
            d,
            time,
            medium: Ptr::null(),
            has_differentials: false,
            differential: RayDifferential::default(),
        }
    }

    pub fn spawn_to_point(p_from: &Point3fi, n: &Normal3f, time: Float, p_to: Point3f) -> Ray {
        let d = p_to - p_from.midpoint();
        Self::spawn(p_from, n, time, d)
    }

    pub fn spawn_to_interaction(
        p_from: &Point3fi,
        n_from: &Normal3f,
        time: Float,
        p_to: &Point3fi,
        n_to: &Normal3f,
    ) -> Ray {
        let dir_for_offset = p_to.midpoint() - p_from.midpoint();
        let pf = Self::offset_origin(p_from, n_from, &dir_for_offset);
        let pt = Self::offset_origin(p_to, n_to, &(pf - p_to.midpoint()));

        let d = pt - pf;

        Ray {
            o: pf,
            d,
            time,
            medium: Ptr::null(),
            has_differentials: false,
            differential: RayDifferential::default(),
        }
    }

    pub fn scale_differentials(&mut self, s: Float) {
        if self.has_differentials {
            let differential = &mut self.differential;
            differential.rx_origin = self.o + (differential.rx_origin - self.o) * s;
            differential.ry_origin = self.o + (differential.ry_origin - self.o) * s;
            differential.rx_direction = self.d + (differential.rx_direction - self.d) * s;
            differential.ry_direction = self.d + (differential.ry_direction - self.d) * s;
        }
    }
}

#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct RayDifferential {
    pub rx_origin: Point3f,
    pub ry_origin: Point3f,
    pub rx_direction: Vector3f,
    pub ry_direction: Vector3f,
}
