use crate::core::geometry::{
    Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f,
    Vector3fi, VectorLike,
};
use crate::core::geometry::{Frame, SqrtExt, spherical_direction};
use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction};
use crate::core::pbrt::gamma;
use crate::core::shape::{
    QuadricIntersection, ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait,
};
use crate::utils::Transform;
use crate::utils::interval::Interval;
use crate::utils::math::{clamp, difference_of_products, radians, safe_acos, safe_sqrt, square};
use crate::utils::sampling::sample_uniform_sphere;
use crate::{Float, PI};
use num_traits::Float as NumFloat;

use core::mem;

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct SphereShape {
    pub radius: Float,
    pub z_min: Float,
    pub z_max: Float,
    pub theta_z_min: Float,
    pub theta_z_max: Float,
    pub phi_max: Float,
    pub render_from_object: Transform,
    pub object_from_render: Transform,
    pub reverse_orientation: bool,
    pub transform_swap_handedness: bool,
}

impl Default for SphereShape {
    fn default() -> Self {
        Self::new(
            Transform::default().into(),
            Transform::default().into(),
            false,
            1.0,
            -1.0,
            1.0,
            360.0,
        )
    }
}

impl SphereShape {
    pub fn new(
        render_from_object: Transform,
        object_from_render: Transform,
        reverse_orientation: bool,
        radius: Float,
        z_min: Float,
        z_max: Float,
        phi_max: Float,
    ) -> Self {
        let theta_z_min = clamp(z_min.min(z_max) / radius, -1., 1.).acos();
        let theta_z_max = clamp(z_max.min(z_max) / radius, -1., 1.).acos();
        let phi_max = radians(clamp(phi_max, 0., 360.0));
        Self {
            render_from_object: render_from_object.clone(),
            object_from_render: object_from_render.clone(),
            radius,
            z_min: clamp(z_min.min(z_max), -radius, radius),
            z_max: clamp(z_min.max(z_max), -radius, radius),
            theta_z_max,
            theta_z_min,
            phi_max,
            reverse_orientation,
            transform_swap_handedness: render_from_object.swaps_handedness(),
        }
    }

    fn basic_intersect(&self, ray: &Ray, t_max: Float) -> Option<QuadricIntersection> {
        let oi: Point3fi = self
            .object_from_render
            .apply_to_interval(&Point3fi::new_from_point(ray.o));
        let di: Vector3fi = self
            .object_from_render
            .apply_to_interval(&Point3fi::new_from_point(Point3f::from(ray.d)))
            .into();
        let a: Interval = square(di.x()) + square(di.y()) + square(di.z());
        let b: Interval = 2. * (di.x() * oi.x() + di.y() * oi.y() + di.z() * oi.z());
        let c: Interval =
            square(oi.x()) + square(oi.y()) + square(oi.z()) - square(Interval::new(self.radius));

        let v: Vector3fi = (oi - b / Vector3fi::from((2. * a) * di)).into();
        let length: Interval = v.norm();
        let discrim =
            4. * a * (Interval::new(self.radius) + length) * (Interval::new(self.radius) - length);
        if discrim.low < 0. {
            return None;
        }

        let root_discrim = discrim.sqrt_ext();

        let q = if Float::from(b) < 0. {
            -0.5 * (b - root_discrim)
        } else {
            -0.5 * (b + root_discrim)
        };

        let mut t0 = q / a;
        let mut t1 = c / q;
        if t0.low > t1.low {
            mem::swap(&mut t0, &mut t1);
        }

        if t0.high > t_max || t1.low < 0. {
            return None;
        }
        let mut t_shape_hit = t0;
        if t_shape_hit.low <= 0. {
            t_shape_hit = t1;
            if t_shape_hit.high > t_max {
                return None;
            }
        }

        let mut p_hit = Point3f::from(oi) + Float::from(t_shape_hit) * Vector3f::from(di);
        if p_hit.x() == 0. && p_hit.y() == 0. {
            p_hit[0] = 1e-5 * self.radius;
        }

        let mut phi = p_hit.y().atan2(p_hit.x());
        if phi < 0. {
            phi += 2. * PI;
        }

        if self.z_min > -self.radius && p_hit.z() < self.z_min
            || self.z_max < self.radius && p_hit.z() > self.z_max
            || phi > self.phi_max
        {
            if t_shape_hit == t1 {
                return None;
            }
            if t1.high > t_max {
                return None;
            }
            t_shape_hit = t1;
            let mut p_hit_vec =
                Vector3f::from(Point3f::from(oi) + Float::from(t_shape_hit) * Vector3f::from(di));
            p_hit_vec *= self.radius / p_hit.distance(Point3f::zero());
            p_hit = Point3f::from(p_hit_vec);

            if p_hit.x() == 0. && p_hit.y() == 0. {
                p_hit[0] = 1e-5 * self.radius;
            }
            phi = p_hit.y().atan2(p_hit.x());
            if phi < 0. {
                phi += 2. * PI;
            }

            if self.z_min > -self.radius && p_hit.z() < self.z_min
                || self.z_max < self.radius && p_hit.z() > self.z_max
                || phi > self.phi_max
            {
                return None;
            }
        }

        Some(QuadricIntersection::new(t_shape_hit.into(), p_hit, phi))
    }

    fn interaction_from_intersection(
        &self,
        isect: QuadricIntersection,
        wo: Vector3f,
        time: Float,
    ) -> SurfaceInteraction {
        let p_hit = isect.p_obj;
        let phi = isect.phi;
        let u = phi / self.phi_max;
        let cos_theta = p_hit.z() / self.radius;
        let theta = safe_acos(cos_theta);
        let v = (theta - self.theta_z_min) / (self.theta_z_max - self.theta_z_min);
        let z_radius = (square(p_hit.x()) + square(p_hit.y())).sqrt();
        let cos_phi = p_hit.x() / z_radius;
        let sin_phi = p_hit.y() / z_radius;
        let dpdu = Vector3f::new(-self.phi_max * p_hit.y(), self.phi_max * p_hit.x(), 0.);
        let sin_theta = safe_sqrt(1. - square(cos_theta));
        let dpdv = (self.theta_z_max - self.theta_z_min)
            * Vector3f::new(
                p_hit.z() * cos_phi,
                p_hit.z() * sin_phi,
                -self.radius * sin_theta,
            );
        let d2pduu = -self.phi_max * self.phi_max * Vector3f::new(p_hit.x(), p_hit.y(), 0.);
        let d2pduv = (self.theta_z_max - self.theta_z_min)
            * p_hit.z()
            * self.phi_max
            * Vector3f::new(-sin_phi, cos_phi, 0.);
        let d2pdvv = -square(self.theta_z_max - self.theta_z_min) * Vector3f::from(p_hit);
        let e = dpdu.dot(dpdu);
        let f = dpdu.dot(dpdv);
        let g = dpdv.dot(dpdv);
        let n = dpdu.cross(dpdv).normalize();
        let e_min = n.dot(d2pduu);
        let f_min = n.dot(d2pduv);
        let g_min = n.dot(d2pdvv);

        let efg2 = difference_of_products(e, g, f, f);
        let inv_efg2 = if efg2 == 0. { 0. } else { 1. / efg2 };
        let dndu = Normal3f::from(
            (f_min * f - e_min * g) * inv_efg2 * dpdu + (e_min * f - f_min * e) * inv_efg2 * dpdv,
        );
        let dndv = Normal3f::from(
            (g_min * f - f_min * g) * inv_efg2 * dpdu + (f_min * f - g_min * e) * inv_efg2 * dpdv,
        );
        let p_error = gamma(5) * Vector3f::from(p_hit).abs();
        let flip_normal = self.reverse_orientation ^ self.transform_swap_handedness;
        let wo_object = self.object_from_render.apply_to_vector(wo);
        SurfaceInteraction::new(
            Point3fi::new_with_error(p_hit, p_error),
            Point2f::new(u, v),
            wo_object,
            dpdu,
            dpdv,
            dndu,
            dndv,
            time,
            flip_normal,
        )
    }
}

impl ShapeTrait for SphereShape {
    fn bounds(&self) -> Bounds3f {
        self.render_from_object
            .apply_to_bounds(Bounds3f::from_points(
                Point3f::new(-self.radius, -self.radius, self.z_min),
                Point3f::new(self.radius, self.radius, self.z_max),
            ))
    }

    fn normal_bounds(&self) -> DirectionCone {
        DirectionCone::entire_sphere()
    }

    fn area(&self) -> Float {
        self.phi_max * self.radius * (self.z_max - self.z_min)
    }

    fn pdf(&self, _interaction: &Interaction) -> Float {
        1. / self.area()
    }

    fn intersect(&self, ray: &Ray, t_max: Option<Float>) -> Option<ShapeIntersection> {
        let t = t_max.unwrap_or(Float::INFINITY);
        if let Some(isect) = self.basic_intersect(ray, t) {
            let intr = self.interaction_from_intersection(isect.clone(), -ray.d, ray.time);
            Some(ShapeIntersection::new(intr, isect.t_hit))
        } else {
            None
        }
    }

    fn intersect_p(&self, ray: &Ray, t_max: Option<Float>) -> bool {
        if let Some(t) = t_max {
            self.basic_intersect(ray, t).is_some()
        } else {
            self.basic_intersect(ray, Float::INFINITY).is_some()
        }
    }

    fn pdf_from_context(&self, ctx: &ShapeSampleContext, wi: Vector3f) -> Float {
        let p_center = self
            .object_from_render
            .apply_to_point(Point3f::new(0., 0., 0.));
        let p_origin = ctx.offset_ray_origin(p_center.into());
        // Return solid angle PDF for point inside sphere
        if p_origin.distance_squared(p_center) <= square(self.radius) {
            let ray = ctx.spawn_ray(wi);
            let isect = self.intersect(&ray, None).expect("Return 0");
            let absdot = isect.intr.n().dot(-Normal3f::from(wi));
            // Compute PDF in solid angle measure from shape intersection point
            let pdf = (1. / self.area()) / (absdot / ctx.p().distance_squared(isect.intr.p()));
            if pdf.is_infinite() {
                return 0.;
            }
            return pdf;
        }
        let sin2_theta_max = self.radius * self.radius / ctx.p().distance_squared(p_center);
        let cos_theta_max = safe_sqrt(1. - sin2_theta_max);
        let mut one_minus_cos_theta_max = 1. - cos_theta_max;
        // Compute more accurate cos theta max for small solid angle
        if sin2_theta_max < 0.00068523 {
            one_minus_cos_theta_max = sin2_theta_max / 2.;
        }

        1. / (2. * PI * one_minus_cos_theta_max)
    }

    fn sample(&self, u: Point2f) -> Option<ShapeSample> {
        let p_obj = Point3f::new(0., 0., 0.) + self.radius * sample_uniform_sphere(u);
        let mut p_obj_vec = Vector3f::from(p_obj);
        p_obj_vec *= self.radius / p_obj.distance(Point3f::zero());
        let p_obj_error = gamma(5) * p_obj_vec.abs();
        let n_obj = Normal3f::from(p_obj_vec);
        let mut n = self.render_from_object.apply_to_normal(n_obj).normalize();
        if self.reverse_orientation {
            n *= -1.;
        }
        let theta = safe_acos(p_obj_vec.z() / self.radius);
        let mut phi = p_obj_vec.y().atan2(p_obj_vec.x());
        if phi < 0. {
            phi += 2. * PI;
        }
        let uv = Point2f::new(
            phi / self.phi_max,
            (theta - self.theta_z_min) / (self.theta_z_max - self.theta_z_min),
        );
        let pi = self
            .render_from_object
            .apply_to_interval(&Point3fi::new_with_error(
                Point3f::from(p_obj_vec),
                p_obj_error,
            ));
        let si = SurfaceInteraction::new_simple(pi, n, uv);
        Some(ShapeSample {
            intr: Interaction::Surface(si),
            pdf: 1. / self.area(),
        })
    }

    fn sample_from_context(&self, ctx: &ShapeSampleContext, u: Point2f) -> Option<ShapeSample> {
        let p_center = self.render_from_object.apply_to_point(Point3f::zero());
        let p_origin = ctx.offset_ray_origin_from_point(p_center);
        if p_origin.distance_squared(p_center) <= square(self.radius) {
            let mut ss = self.sample(u)?;
            ss.intr.get_common_mut().time = ctx.time;
            let mut wi = ss.intr.p() - ctx.p();
            if wi.norm_squared() == 0. {
                return None;
            }
            wi = wi.normalize();
            ss.pdf =
                Vector3f::from(ss.intr.n()).dot(-wi).abs() / ctx.p().distance_squared(ss.intr.p());
            if ss.pdf.is_infinite() {
                return None;
            }
            return Some(ss);
        }
        let sin_theta_max = self.radius / ctx.p().distance(p_center);
        let sin2_theta_max = square(sin_theta_max);
        let cos_theta_max = safe_sqrt(1. - sin2_theta_max);
        let mut one_minus_cos_theta_max = 1. - cos_theta_max;
        let mut cos_theta = (cos_theta_max - 1.) * u[0] + 1.;
        let mut sin2_theta = 1. - square(cos_theta);

        // Compute more accurate cos theta max for small solid angle
        if sin2_theta_max < 0.00068523 {
            sin2_theta = sin2_theta_max * u[0];
            cos_theta = (1. - sin2_theta).sqrt();
            one_minus_cos_theta_max = sin2_theta_max / 2.;
        }
        // Compute angle alpha from center of sphere to sampled point on surface
        let cos_alpha = sin2_theta / sin_theta_max
            + cos_theta * safe_sqrt(1. - sin2_theta / square(sin_theta_max));
        let sin_alpha = safe_sqrt(1. - square(cos_alpha));
        let phi = u[1] * 2. * PI;
        let w = spherical_direction(sin_alpha, cos_alpha, phi);
        let sampling_frame = Frame::from_z((p_center - ctx.p()).normalize());
        let mut n: Normal3f = sampling_frame.from_local(-w).into();
        let p = p_center + self.radius * Vector3f::from(n);
        if self.reverse_orientation {
            n *= -1.;
        }
        let p_error = gamma(5) * Vector3f::from(p).abs();
        // Compute (u, v) coordinates for sampled point on sphere
        let p_obj = self.object_from_render.apply_to_point(Point3f::from(p));
        let theta = safe_acos(p_obj.z() / self.radius);
        let mut sphere_phi = p_obj.y().atan2(p_obj.x());
        if sphere_phi < 0. {
            sphere_phi += 2. * PI;
        }
        let uv = Point2f::new(
            sphere_phi / self.phi_max,
            (theta - self.theta_z_min) / (self.theta_z_max - self.theta_z_min),
        );
        let pi = Point3fi::new_with_error(p_obj, p_error);
        let intr = SurfaceInteraction::new_simple(pi, n, uv);
        Some(ShapeSample {
            intr: Interaction::Surface(intr),
            pdf: 1. / (2. * PI * one_minus_cos_theta_max),
        })
    }
}
