use core::{iter, mem};
use hashbrown::HashMap;
use num::traits::Float;
pub use vek::{geom::repr_simd::*, mat::repr_simd::column_major::Mat4, ops::*, vec::repr_simd::*};
//pub use vek::{geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*,
// vec::repr_c::*};

pub fn aabb_to_points<T: Float>(bounds: Aabb<T>) -> [Vec3<T>; 8] {
    [
        Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z),
        Vec3::new(bounds.max.x, bounds.min.y, bounds.min.z),
        Vec3::new(bounds.max.x, bounds.max.y, bounds.min.z),
        Vec3::new(bounds.min.x, bounds.max.y, bounds.min.z),
        Vec3::new(bounds.min.x, bounds.min.y, bounds.max.z),
        Vec3::new(bounds.max.x, bounds.min.y, bounds.max.z),
        Vec3::new(bounds.max.x, bounds.max.y, bounds.max.z),
        Vec3::new(bounds.min.x, bounds.max.y, bounds.max.z),
    ]
}

/// Each Vec4 <a, b, c, -d> should be interpreted as reprenting plane
/// equation
///
/// a(x - x0) + b(y - y0) + c(z - z0) = 0, i.e.
/// ax + by + cz - (a * x0 + b * y0 + c * z0) = 0, i.e.
/// ax + by + cz = (a * x0 + b * y0 + c * z0), i.e.
/// (letting d = a * x0 + b * y0 + c * z0)
/// ax + by + cz = d
///
/// where d is the distance of the plane from the origin.
pub fn aabb_to_planes<T: Float>(bounds: Aabb<T>) -> [Vec4<T>; 6] {
    let zero = T::zero();
    let one = T::one();
    let bounds = bounds.map(|e| e.abs());
    [
        // bottom
        Vec4::new(zero, -one, zero, -bounds.min.y),
        // top
        Vec4::new(zero, one, zero, -bounds.max.y),
        // left
        Vec4::new(-one, zero, zero, -bounds.min.x),
        // right
        Vec4::new(one, zero, zero, -bounds.max.x),
        // near
        Vec4::new(zero, zero, -one, -bounds.min.z),
        // far
        Vec4::new(zero, zero, one, -bounds.max.z),
    ]
}

pub fn mat_mul_points<T: Float + MulAdd<T, T, Output = T>>(
    mat: Mat4<T>,
    pts: &mut [Vec3<T>],
    mut do_p: impl FnMut(Vec4<T>) -> Vec3<T>,
) {
    pts.iter_mut().for_each(|p| {
        *p = do_p(mat * Vec4::from_point(*p));
    });
}

/// NOTE: Expects points computed from aabb_to_points.
pub fn calc_view_frust_object<T: Float>(pts: &[Vec3<T>; 8]) -> Vec<Vec<Vec3<T>>> {
    vec![
        // near (CCW)
        vec![pts[0], pts[1], pts[2], pts[3]],
        // far (CCW)
        vec![pts[7], pts[6], pts[5], pts[4]],
        // left (CCW)
        vec![pts[0], pts[3], pts[7], pts[4]],
        // right (CCW)
        vec![pts[1], pts[5], pts[6], pts[2]],
        // bottom (CCW)
        vec![pts[4], pts[5], pts[1], pts[0]],
        // top (CCW)
        vec![pts[6], pts[7], pts[3], pts[2]],
    ]
}

pub fn calc_view_frustum_world_coord<T: Float + MulAdd<T, T, Output = T>>(
    inv_proj_view: Mat4<T>,
) -> [Vec3<T>; 8] {
    let mut world_pts = aabb_to_points(Aabb {
        min: -Vec3::one(),
        max: Vec3::one(),
    });
    mat_mul_points(inv_proj_view, &mut world_pts, |p| Vec3::from(p) / p.w);
    world_pts
}

pub fn point_plane_distance<T: Float>(point: Vec3<T>, norm_dist: Vec4<T>) -> T {
    norm_dist.dot(Vec4::from_point(point))
}

pub fn point_before_plane<T: Float>(point: Vec3<T>, plane: Vec4<T>) -> bool {
    point_plane_distance(point, plane) > T::zero()
}

/// Returns true if and only if the final point in the polygon (i.e. the
/// first point added to the new polygon) is outside the clipping plane
/// (this implies that the polygon must be non-degenerate).
pub fn clip_points_by_plane<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
    points: &mut Vec<Vec3<T>>,
    plane: Vec4<T>,
    intersection_points: &mut Vec<Vec3<T>>,
) -> bool {
    if points.len() < 3 {
        return false;
    }
    // NOTE: Guaranteed to succeed since points.len() > 3.
    let mut current_point = points[points.len() - 1];
    let intersect_plane_edge = |a, b| {
        let diff: Vec3<_> = b - a;
        let t = plane.dot(Vec4::from_direction(diff));
        if t == T::zero() {
            None
        } else {
            let t = -(plane.dot(Vec4::from_point(a)) / t);
            if t < T::zero() || T::one() < t {
                None
            } else {
                Some(diff * t + a)
            }
        }
    };
    let last_is_outside = point_before_plane(current_point, plane);
    let mut is_outside = last_is_outside;
    let mut old_points = Vec::with_capacity((3 * points.len()) / 2);
    mem::swap(&mut old_points, points);
    old_points.into_iter().for_each(|point| {
        let prev_point = mem::replace(&mut current_point, point);
        let before_plane = point_before_plane(current_point, plane);
        let prev_is_outside = mem::replace(&mut is_outside, before_plane);
        if !prev_is_outside {
            // Push previous point.
            points.push(prev_point);
        }
        if prev_is_outside != is_outside {
            if let Some(intersection_point) = intersect_plane_edge(prev_point, current_point) {
                // Push intersection point.
                intersection_points.push(intersection_point);
                points.push(intersection_point);
            }
        }
    });
    last_is_outside
}

fn append_intersection_points<T: Float + core::fmt::Debug>(
    polys: &mut Vec<Vec<Vec3<T>>>,
    intersection_points: Vec<Vec3<T>>,
    tolerance: T,
) {
    // NOTE: We use decoded versions of each polygon, with rounded entries.
    //
    // The line segments in intersection_points are consistently ordered as follows:
    // each segment represents the intersection of the cutting plane with the
    // polygon from which the segment came.  The polygon can thus be split into
    // two parts: the part "inside" the new surface (below the plane), and the
    // part "outside" it (above the plane).  Thus, when oriented
    // with the polygon normal pointing into the camera, and the cutting plane as
    // the x axis, with the "outside" part on top and the "inside" part on the
    // bottom, there is a leftmost point (the point going from outside to
    // inside, counterclockwise) and a rightmost point (the point going from
    // inside to outside, counterclockwise).  Our consistent ordering guarantees
    // that the leftmost point comes before the rightmost point in each line
    // segment.
    //
    // Why does this help us?  To see that, consider the polygon adjacent to the
    // considered polygon which also has the same right intersection point (we
    // know there will be exactly one of these, because we have a solid
    // structure and are only considering polygons that intersect the plane
    // exactly two times; this means that we are ignoring polygons that intersect
    // the plane at just one point, which means the two polygons must share a
    // point, not be coplanar, and both intersect the plane; no theorem here,
    // but I believe there can provably be at most one such instance given that
    // we have at least three polygons with such a line segment).
    //
    // Now, for the adjacent polygon, repeat the above process.  If the intersection
    // point shared by the polygons is on the right in both cases, then we can
    // see that the polygon's normal must be facing in the opposite direction of
    // the original polygon despite being adjacent.  But this
    // should be impossible for a closed object!  The same applies to the leftmost
    // point.
    //
    // What is the practical upshot of all this?  It means that we can consistently
    // hash each line segment by its first point, which we can look up using the
    // second point of a previous line segment.  This will produce a chain of
    // entries terminating in the original segment we looked up.  As an added
    // bonus, by going from leftmost point to leftmost point, we also ensure that
    // we produce a polygon whose face is oriented counterclockwise around its
    // normal; this can be seen by following the right-hand rule (TODO: provide
    // more rigorous proof).
    let tol = tolerance.recip();
    let make_key = move |point: Vec3<T>| {
        // We use floating points rounded to tolerance in order to make our HashMap
        // lookups work. Otherwise we'd have to use a sorted structure, like a
        // btree, which wouldn't be the end of the world but would have
        // theoretically worse complexity.
        //
        // NOTE: Definitely non-ideal that we panic if the rounded value can't fit in an
        // i64...
        //
        // TODO: If necessary, let the caller specify how to hash these keys, since in
        // cases where we know the kind of floating point we're using we can
        // just cast to bits or something.
        point.map(|e| {
            (e * tol)
                .round()
                .to_i64()
                .expect("We don't currently try to handle floats that won't fit in an i64.")
        })
    };
    let mut lines_iter = intersection_points.chunks_exact(2).filter_map(|uv| {
        let u_key = make_key(uv[0]);
        let v = uv[1];
        // NOTE: The reason we need to make sure this doesn't happen is that it's
        // otherwise possible for two points to hash to the same value due to
        // epsilon being too low. Because of the ordering mentioned previously,
        // we know we should *eventually* find a pair of points starting with
        // make_key(u) and ending with a different make_key(v) in such cases, so
        // we just discard all the other pairs (treating them as points rather
        // than lines).
        (u_key != make_key(v)).then_some((u_key, v))
    });

    if let Some((last_key, first)) = lines_iter.next() {
        let lines = lines_iter.collect::<HashMap<_, _>>();
        if lines.len() < 2 {
            // You need at least 3 sides for a polygon
            return;
        }
        // NOTE: Guaranteed to terminate, provided we have no cycles besides the one
        // that touches every point (which should be the case given how these
        // points were generated).
        let poly_iter = iter::successors(Some(first), |&cur| lines.get(&make_key(cur)).copied());
        let poly: Vec<_> = poly_iter.collect();
        // We have to check to make sure we really went through the whole cycle.
        // TODO: Consider adaptively decreasing precision until we can make the cycle
        // happen.
        if poly.last().copied().map(make_key) == Some(last_key) {
            // Push the new polygon onto the object.
            polys.push(poly);
        }
    }
}

pub fn clip_object_by_plane<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
    polys: &mut Vec<Vec<Vec3<T>>>,
    plane: Vec4<T>,
    tolerance: T,
) {
    let mut intersection_points = Vec::new();
    polys.drain_filter(|points| {
        let len = intersection_points.len();
        let outside_first = clip_points_by_plane(points, plane, &mut intersection_points);
        // Only remember intersections that are not coplanar with this side; i.e. those
        // that have segment length 2.
        if len + 2 != intersection_points.len() {
            intersection_points.truncate(len);
        } else if !outside_first {
            // Order the two intersection points consistently, so that, when considered
            // counterclockwise:
            // - the first point goes from the exterior of the polygon (above the cutting
            //   plane) to its interior.
            // - the second point goes from the interior of the polygon (below the cutting
            //   plane) to its exterior.
            // the second is always going
            //
            // This allows us to uniquely map each line segment to an "owning" point (the
            // one going from outside to inside), which happens to also point
            // the segment in a counterclockwise direction around the new
            // polygon normal composed of all the lines we clipped.
            intersection_points.swap(len, len + 1);
        }
        // Remove polygon if it was clipped away
        points.is_empty()
    });
    // Add a polygon of all intersection points with the plane to close out the
    // object.
    append_intersection_points(polys, intersection_points, tolerance);
}

pub fn clip_object_by_aabb<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
    polys: &mut Vec<Vec<Vec3<T>>>,
    bounds: Aabb<T>,
    tolerance: T,
) {
    let planes = aabb_to_planes(bounds);
    planes.iter().for_each(|&plane| {
        clip_object_by_plane(polys, plane, tolerance);
    });
}

/// Return value is 'Some(segment)' if line segment intersects the current
/// test plane.  Otherwise 'None' is returned in which case the line
/// segment is entirely clipped.
pub fn clip_test<T: Float + core::fmt::Debug>(p: T, q: T, (u1, u2): (T, T)) -> Option<(T, T)> {
    if p == T::zero() {
        if q >= T::zero() { Some((u1, u2)) } else { None }
    } else {
        let r = q / p;
        if p < T::zero() {
            if r > u2 {
                None
            } else {
                Some((if r > u1 { r } else { u1 }, u2))
            }
        } else if r < u1 {
            None
        } else {
            Some((u1, if r < u2 { r } else { u2 }))
        }
    }
}

pub fn intersection_line_aabb<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
    p: Vec3<T>,
    dir: Vec3<T>,
    bounds: Aabb<T>,
) -> Option<Vec3<T>> {
    clip_test(-dir.z, p.z - bounds.min.z, (T::zero(), T::infinity()))
        .and_then(|t| clip_test(dir.z, bounds.max.z - p.z, t))
        .and_then(|t| clip_test(-dir.y, p.y - bounds.min.y, t))
        .and_then(|t| clip_test(dir.y, bounds.max.y - p.y, t))
        .and_then(|t| clip_test(-dir.x, p.x - bounds.min.x, t))
        .and_then(|t| clip_test(dir.x, bounds.max.x - p.x, t))
        .and_then(|(t1, t2)| {
            if T::zero() <= t2 {
                Some(dir.mul_add(Vec3::broadcast(t2), p))
            } else if T::zero() <= t1 {
                Some(dir.mul_add(Vec3::broadcast(t1), p))
            } else {
                None
            }
        })
}

pub fn include_object_light_volume<
    T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug,
    I: Iterator<Item = Vec3<T>>,
>(
    obj: I,
    light_dir: Vec3<T>,
    bounds: Aabb<T>,
) -> impl Iterator<Item = Vec3<T>> {
    obj.flat_map(move |pt| iter::once(pt).chain(intersection_line_aabb(pt, -light_dir, bounds)))
}

// NOTE: Currently specialized to skip extending to the end of the light ray,
// since our light ray is already infinite.  Correct code is commented out
// below.
pub fn calc_focused_light_volume_points<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
    inv_proj_view: Mat4<T>,
    _light_dir: Vec3<T>,
    scene_bounding_box: Aabb<T>,
    tolerance: T,
) -> impl Iterator<Item = Vec3<T>> {
    let world_pts = calc_view_frustum_world_coord(inv_proj_view);
    let mut world_frust_object = calc_view_frust_object(&world_pts);
    clip_object_by_aabb(&mut world_frust_object, scene_bounding_box, tolerance);
    world_frust_object.into_iter().flat_map(|e| e.into_iter())
    /* include_object_light_volume(
        world_frust_object.into_iter().flat_map(|e| e.into_iter()),
        light_dir,
        scene_bounding_box,
    ) */
}

/// NOTE: Will not yield useful results if pts is empty!
pub fn fit_psr<
    T: Float + MulAdd<T, T, Output = T>,
    I: Iterator<Item = Vec3<T>>,
    F: FnMut(Vec4<T>) -> Vec4<T>,
>(
    mat: Mat4<T>,
    pts: I,
    mut do_p: F,
) -> Aabb<T> {
    let mut min = Vec4::broadcast(T::infinity());
    let mut max = Vec4::broadcast(T::neg_infinity());
    pts.map(|p| do_p(mat * Vec4::<T>::from_point(p)))
        .for_each(|p| {
            min = Vec4::partial_min(min, p);
            max = Vec4::partial_max(max, p);
        });
    Aabb {
        min: min.xyz(),
        max: max.xyz(),
    }
}
