use std::sync::Arc;

use ray_tracing_core::{affine_transform::AffineTransform, prelude::*, scene::Intersection};

use crate::{
    AreaLight,
    bvh::{Bvh, BvhData, BvhIntersection},
    either::Either,
    material::PbrtMaterial,
    scene::UVMaterial,
};

#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct Shape<R: Rng> {
    pub(crate) ctm: AffineTransform,
    pub(crate) aabb: AABB,
    pub(crate) material: Either<Arc<dyn PbrtMaterial<R>>, AreaLight>,
    pub(crate) obj: ShapeType,
    pub(crate) alpha: ShapeAlpha,
}

impl<R: Rng> Shape<R> {
    pub(crate) fn new(
        ctm: AffineTransform,
        material: Either<Arc<dyn PbrtMaterial<R>>, AreaLight>,
        obj: ShapeType,
        alpha: ShapeAlpha,
    ) -> Self {
        let aabb = match &obj {
            ShapeType::TriangleMesh(bvh) => bvh.inner().p[2..]
                .iter()
                .fold(AABB::new(bvh.inner().p[0], bvh.inner().p[1]), |a, &p| {
                    a.extend(p)
                }),
            ShapeType::BilinearMesh { p, .. } => p[2..]
                .iter()
                .fold(AABB::new(p[0], p[1]), |a, &p| a.extend(p)),
            _ => todo!(),
        };
        Shape {
            ctm,
            aabb,
            material,
            obj,
            alpha,
        }
    }
}

#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum ShapeAlpha {
    None,
    Value(Float),
    Texture(String),
}

pub(crate) struct InnerIntersect {
    t: Float,
    n: Dir3,
    u: Float,
    v: Float,
}

impl BvhIntersection for InnerIntersect {
    fn get_t(&self) -> Float {
        self.t
    }
}
#[derive(Debug)]
pub(crate) struct TriangleMesh {
    pub(crate) indices: Vec<[usize; 3]>,
    pub(crate) p: Vec<Pos3>,
    pub(crate) n: Vec<Dir3>,
    #[allow(dead_code)]
    pub(crate) s: Vec<Dir3>,
    pub(crate) uv: Vec<[Float; 2]>,
}

impl BvhData for TriangleMesh {
    type Intersect<'a> = InnerIntersect;

    fn len(&self) -> crate::bvh::Index {
        self.indices.len() as crate::bvh::Index
    }

    fn sort_range_dim(&mut self, id: std::ops::Range<crate::bvh::Index>, dim: u8) {
        let get_key =
            |t: &[usize; 3]| t.iter().map(|&v| self.p[v][dim as usize]).sum::<Float>() / 3.0;
        self.indices[id.start as usize..id.end as usize]
            .sort_by(|a, b| get_key(a).partial_cmp(&get_key(b)).unwrap());
    }

    fn get_aabb(&self, id: crate::bvh::Index) -> AABB {
        let v = self.indices[id as usize];

        AABB::new(self.p[v[0]], self.p[v[1]]).extend(self.p[v[2]])
    }

    fn intersect(
        &self,
        id: crate::bvh::Index,
        ray: Ray,
        min: Float,
        max: Float,
    ) -> Option<Self::Intersect<'_>> {
        let _ = max;
        let _ = min;
        let v = [
            self.p[self.indices[id as usize][0]],
            self.p[self.indices[id as usize][1]],
            self.p[self.indices[id as usize][2]],
        ];

        let e1 = v[1] - v[0];
        let e2 = v[2] - v[0];

        let ray_cross_e2 = Dir3::cross(ray.dir(), e2);
        let det = e1.dot(ray_cross_e2);

        if det > -f32::EPSILON && det < f32::EPSILON {
            return None; // This ray is parallel to this triangle.
        }

        let inv_det = 1.0 / det;
        let s = ray.start() - v[0];
        let u = inv_det * s.dot(ray_cross_e2);
        if !(0.0..=1.0).contains(&u) {
            return None;
        }

        let s_cross_e1 = s.cross(e1);
        let v = inv_det * Dir3::dot(ray.dir(), s_cross_e1);
        if v < 0.0 || u + v > 1.0 {
            return None;
        }
        // At this stage we can compute t to find out where the intersection point is on the line.
        let t = inv_det * e2.dot(s_cross_e1);

        if t > Float::EPSILON {
            // ray intersection
            let w = 1.0 - u - v;
            Some(InnerIntersect {
                t,
                n: if self.n.is_empty() {
                    Dir3::cross(e1, e2)
                } else {
                    w * self.n[self.indices[id as usize][0]]
                        + u * self.n[self.indices[id as usize][1]]
                        + v * self.n[self.indices[id as usize][2]]
                },
                u: if self.uv.is_empty() {
                    0.0
                } else {
                    w * self.uv[self.indices[id as usize][0]][0]
                        + u * self.uv[self.indices[id as usize][1]][0]
                        + v * self.uv[self.indices[id as usize][2]][0]
                },
                v: if self.uv.is_empty() {
                    0.0
                } else {
                    w * self.uv[self.indices[id as usize][0]][1]
                        + u * self.uv[self.indices[id as usize][1]][1]
                        + v * self.uv[self.indices[id as usize][2]][1]
                },
            })
        } else {
            // This means that there is a line intersection but not a ray intersection.
            None
        }
    }
}

#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum ShapeType {
    Sphere {
        radius: Float,
        zmin: Float,
        zmax: Float,
        phimax: Float,
    },
    TriangleMesh(Bvh<TriangleMesh>),
    BilinearMesh {
        indices: Vec<usize>,
        p: Vec<Pos3>,
        n: Vec<Dir3>,
        uv: Vec<[Float; 2]>,
    },
    Disk {
        height: Float,
        radius: Float,
        innerradius: Float,
        phimax: Float,
    },
    PlyMesh {
        filename: String,
        displacement: Option<String>,
        edgelength: Float,
    },
}

fn quadratic(a: Float, b: Float, c: Float) -> (Option<Float>, Option<Float>) {
    if a == 0.0 {
        if b == 0.0 {
            return (None, None);
        }
        return (Some(-c / b), None);
    }

    let discrim = b * b - 4.0 * a * c;

    if discrim < 0.0 {
        return (None, None);
    }

    let root_discrim = Float::sqrt(discrim);

    let q = -0.5 * (b + Float::copysign(root_discrim, b));

    let mut t0 = q / a;
    let mut t1 = c / q;

    if t1 < t0 {
        std::mem::swap(&mut t1, &mut t0);
    }

    (Some(t0), Some(t1))
}

fn bilinear_intersection(
    ray: Ray,
    min: Float,
    max: Float,
    patch: [Pos3; 4],
) -> Option<(Float, Float, Float)> {
    let a = Dir3::dot(
        Dir3::cross(patch[2] - patch[0], patch[1] - patch[3]),
        ray.dir(),
    );
    let c = Dir3::dot(
        Dir3::cross(patch[0] - ray.start(), ray.dir()),
        patch[1] - patch[0],
    );
    let b = Dir3::dot(
        Dir3::cross(patch[2] - ray.start(), ray.dir()),
        patch[3] - patch[2],
    ) - (a + c);

    let (u0, u1) = quadratic(a, b, c);

    [u0, u1]
        .into_iter()
        .flatten()
        .filter(|&u| {
            if (0.0..=1.0).contains(&u) {
                return true;
            }
            false
        })
        .filter_map(|u| {
            // dbg!(a, b, c, u, a * u * u + b * u + c);

            let uo = patch[0].as_dir() * (1.0 - u) + patch[2].as_dir() * u;
            let ud = patch[1].as_dir() * (1.0 - u) + patch[3].as_dir() * u - uo;

            let deltao = uo - ray.start().as_dir();

            let perp = Dir3::cross(ray.dir(), ud);

            let p2 = perp.length_squared();

            let v1 = deltao.x() * ray.dir().y() * perp.z()
                + ray.dir().x() * perp.y() * deltao.z()
                + perp.x() * deltao.y() * ray.dir().z()
                - deltao.x() * perp.y() * ray.dir().z()
                - ray.dir().x() * deltao.y() * perp.z()
                - perp.x() * ray.dir().y() * deltao.z();

            let t1 = deltao.x() * ud.y() * perp.z()
                + ud.x() * perp.y() * deltao.z()
                + perp.x() * deltao.y() * ud.z()
                - deltao.x() * perp.y() * ud.z()
                - ud.x() * deltao.y() * perp.z()
                - perp.x() * ud.y() * deltao.z();

            // dbg!(t1 / p2, v1 / p2);
            if min * p2 <= t1 && t1 <= max * p2 && 0.0 <= v1 && v1 <= p2 {
                Some((t1 / p2, u, v1 / p2))
            } else {
                None
            }
        })
        .next()
}

impl<R: Rng + std::fmt::Debug> Shape<R> {
    fn inner_intersect(&self, ray: Ray, min: Float, max: Float) -> Option<InnerIntersect> {
        match &self.obj {
            &ShapeType::Sphere {
                radius,
                zmin,
                zmax,
                phimax,
            } => {
                let a = ray.dir().length_squared();
                let b = 2.0 * Dir3::dot(ray.dir(), ray.start() - Pos3::zero());
                let c = Dir3::dot(ray.start() - Pos3::zero(), ray.start() - Pos3::zero())
                    - radius * radius;

                let dir = ray.dir().normalize();
                let v = ray.start() - (b / (2.0 * a) * dir);
                let length = (v - Pos3::zero()).length();
                let discrim = 4.0 * a * (radius + length) * (radius - length);

                if discrim > 0.0 {
                    let root_discrim = Float::sqrt(discrim);

                    let q = if b < 0.0 {
                        -0.5 * (b - root_discrim)
                    } else {
                        -0.5 * (b + root_discrim)
                    };

                    let mut t0 = q / a;
                    let mut t1 = c / q;

                    if t1 < t0 {
                        std::mem::swap(&mut t1, &mut t0);
                    }

                    if let Some(t) = [t0, t1].into_iter().find(|&t| {
                        if min <= t && t <= max {
                            let p = ray.at(t0);
                            let mut phi = Float::atan2(p.y(), p.x());
                            if phi < 0.0 {
                                phi += 2.0 * FloatConsts::PI;
                            }
                            if zmin <= p.z() && p.z() <= zmax && phi <= phimax {
                                return true;
                            }
                        }
                        false
                    }) {
                        let p = ray.at(t);
                        let mut phi = Float::atan2(p.y(), p.x());
                        if phi < 0.0 {
                            phi += 2.0 * FloatConsts::PI;
                        }
                        Some(InnerIntersect {
                            t,
                            n: p.as_dir(),
                            u: phi,
                            v: Float::acos(p.z() / radius),
                        })
                    } else {
                        None
                    }
                } else {
                    None
                }
            }
            ShapeType::TriangleMesh(bvh) => bvh.intersect(ray, min, max),
            ShapeType::BilinearMesh {
                indices,
                p,
                n: _,
                uv: _,
            } => {
                if indices.is_empty()
                    && let Some((t, u, v)) =
                        bilinear_intersection(ray, min, max, [p[0], p[1], p[2], p[3]])
                {
                    Some(InnerIntersect {
                        t,
                        n: Dir3::new(0.0, 0.0, 1.0),
                        u,
                        v,
                    })
                } else {
                    None
                }
            }
            _ => None,
        }
    }
    pub(crate) fn intersect(
        &'_ self,
        ray: Ray,
        min: Float,
        max: Float,
    ) -> Option<Intersection<R, UVMaterial<'_, R>, &'_ dyn Light<R>>> {
        let ray = self.ctm.transform_ray(ray);

        self.inner_intersect(ray, min, max)
            .map(|InnerIntersect { t, n, u, v }| {
                Intersection::new(
                    t,
                    self.ctm.inverse_transform_normal(n).normalize(),
                    self.material.get_a().map(|m| UVMaterial {
                        u,
                        v,
                        material: m.as_ref(),
                    }),
                    self.material
                        .get_b()
                        .map(|l| l as &dyn ray_tracing_core::light::Light<R>),
                    0.0,
                )
            })
    }
}
