use core::f32;
use egui::{Color32, ColorImage, Pos2, Rect, Vec2, emath::TSTransform, epaint::Vertex};
use std::ops::Range;

pub trait BlendFn {
    fn blend(a: Color32, b: Color32) -> Color32;
}

pub mod blend {
    pub struct Normal;
    pub struct Add;
    pub struct Multiply;
}

/// Rasterize some triangles onto a new image,
///
/// Triangle positions must be in image-local point-coords.
/// `width` and `height` are in pixel coords.
pub fn rasterize<'a, Blend: BlendFn>(
    width: usize,
    height: usize,
    point_to_pixel: TSTransform,
    triangles: impl Iterator<Item = [&'a Vertex; 3]>,
) -> ColorImage {
    let mut image = ColorImage::filled([width, height], Color32::TRANSPARENT);
    rasterize_onto::<Blend>(&mut image, point_to_pixel, triangles);
    image
}

/// Rasterize some triangles onto an image,
///
/// Triangle positions must be in image-local point-coords.
pub fn rasterize_onto<'a, Blend: BlendFn>(
    image: &mut ColorImage,
    point_to_pixel: TSTransform,
    triangles: impl Iterator<Item = [&'a Vertex; 3]>,
) {
    let width = image.width();
    let height = image.height();

    let mut set_pixel = |x: usize, y: usize, color| {
        let pixel = &mut image.pixels[y * width + x];
        *pixel = Blend::blend(*pixel, color);
    };

    let image_box = PxBoundingBox {
        x_from: 0,
        y_from: 0,
        x_to: width,
        y_to: height,
    };

    let pixel_to_point = point_to_pixel.inverse();

    for triangle in triangles {
        let [a, b, c] = triangle;
        if triangle_area(a.pos, b.pos, c.pos) == 0.0 {
            continue;
        }

        // Check all pixels within the triangle's bounding box.
        let bounding_box =
            triangle_bounding_box(&triangle, point_to_pixel).intersection(&image_box);

        // TODO: consider subdividing the triangle if it's very large.
        let pixels = pixels_in_box(bounding_box);

        for [x, y] in pixels {
            // Calculate point-coordinate of the pixel
            let pt_pos = pixel_to_point * Pos2::new(x as f32, y as f32);

            let point_in_triangle = point_in_triangle(pt_pos, triangle);

            // If the pixel is within the triangle, fill it in.
            if point_in_triangle.inside {
                let [c0, c1, c2] = [0, 1, 2].map(|i| {
                    triangle[i]
                        .color
                        .linear_multiply(point_in_triangle.weights[i])
                });

                let color = c0 + c1 + c2;

                set_pixel(x, y, color);
            }
        }
    }
}

/// Rasterize a single triangles onto an image,
///
/// Triangle positions must be in image-local point-coords.
pub fn rasterize_triangle_onto<Blend: BlendFn>(
    image: &mut ColorImage,
    point_to_pixel: TSTransform,
    triangle: [&Vertex; 3],
) {
    rasterize_onto::<Blend>(image, point_to_pixel, [triangle].into_iter());
}

/// A bounding box, measured in pixels.
#[derive(Debug, PartialEq, Eq)]
pub struct PxBoundingBox {
    pub x_from: usize,
    pub y_from: usize,
    pub x_to: usize,
    pub y_to: usize,
}

impl PxBoundingBox {
    pub fn intersection(&self, other: &PxBoundingBox) -> PxBoundingBox {
        PxBoundingBox {
            x_from: self.x_from.max(other.x_from),
            y_from: self.y_from.max(other.y_from),
            x_to: self.x_to.min(other.x_to),
            y_to: self.y_to.min(other.y_to),
        }
    }

    pub fn union(&self, other: &PxBoundingBox) -> PxBoundingBox {
        PxBoundingBox {
            x_from: self.x_from.min(other.x_from),
            y_from: self.y_from.min(other.y_from),
            x_to: self.x_to.max(other.x_to),
            y_to: self.y_to.max(other.y_to),
        }
    }

    pub fn x_range(&self) -> Range<usize> {
        self.x_from..self.x_to
    }

    pub fn y_range(&self) -> Range<usize> {
        self.y_from..self.y_to
    }

    /// Test whether two boxes do NOT overlap
    pub fn overlaps_with(&self, other: &PxBoundingBox) -> bool {
        !self.is_disjoint_from(other)
    }

    pub fn is_disjoint_from(&self, other: &PxBoundingBox) -> bool {
        self.x_from > other.x_to
            || self.y_from > other.y_to
            || other.x_from > self.x_to
            || other.y_from > self.y_to
    }
}

pub fn triangle_bounding_box(
    triangle: &[&Vertex; 3],
    point_to_pixel: TSTransform,
) -> PxBoundingBox {
    // calculate bounding box in point coords
    let mut rect = Rect::NOTHING;
    for vertex in triangle {
        rect.min = rect.min.min(vertex.pos);
        rect.max = rect.max.max(vertex.pos);
    }

    // convert bounding box to pixel coords
    let rect = point_to_pixel.mul_rect(rect);
    PxBoundingBox {
        x_from: rect.min.x.floor() as usize,
        y_from: rect.min.y.floor() as usize,
        x_to: rect.max.x.ceil() as usize,
        y_to: rect.max.y.ceil() as usize,
    }
}

/// Calculate the perpendicular vector (90 degrees from the given vector)
fn perpendicular(v: Vec2) -> Vec2 {
    Vec2::new(v.y, -v.x)
}

#[derive(Clone, Debug)]
struct PointInTriangle {
    /// Is the point inside the triangle?
    inside: bool,

    /// Normalized weights describing the vicinity between the point and the three verticies of
    /// thre triangle.
    weights: [f32; 3],
}

/// Calculate whether a point is within a triangle, and the relative vicinities between the point
/// and each triangle vertex. The triangle must have a non-zero area.
fn point_in_triangle(point: Pos2, triangle: [&Vertex; 3]) -> PointInTriangle {
    let [a, b, c] = triangle;

    let sides = [[b, c], [c, a], [a, b]];

    // For each side of the triangle, imagine a new triangle consisting of the side and `point`.
    // Calculate the areas of those triangles.
    let areas = sides.map(|[start, end]| signed_triangle_area(start.pos, end.pos, point));

    // Use the areas to determine the side of the line at which the point exists.
    // If the area is positive, the point is on the right side of the triangle line.
    let [side_ab, side_bc, side_ca] = areas.map(|area| area >= 0.0);

    // Total area of the traingle.
    let triangle_area: f32 = areas.iter().sum();

    // egui does not wind the triangles in a consistent order, otherwise we might check if the
    // point is on a *specific* side of each line. As it is, we just check if the point is on the
    // same side of each line.
    let inside = side_ab == side_bc && side_bc == side_ca;

    // Normalize the weights.
    let weights = areas.map(|area| area / triangle_area);

    if cfg!(debug_assertions) && weights.into_iter().any(f32::is_nan) {
        panic!("weights must not be NaN! {weights:?} {triangle_area:?} {areas:?} {sides:?}");
    }

    PointInTriangle { inside, weights }
}

/// Calculate the area of a triangle.
fn triangle_area(a: Pos2, b: Pos2, c: Pos2) -> f32 {
    signed_triangle_area(a, b, c).abs()
}

/// Calculate the area of a triangle.
///
/// The area will be positive if the triangle is wound clockwise, and negative otherwise.
fn signed_triangle_area(a: Pos2, b: Pos2, c: Pos2) -> f32 {
    // Vector of an arbitrary "base" side of the triangle.
    let base = c - a;
    let base_perp = perpendicular(base);
    let diagonal = c - b;
    base_perp.dot(diagonal) / 2.0
}

/// Iterate over every pixel coordinate in a box.
#[inline(always)]
fn pixels_in_box(
    PxBoundingBox {
        x_from,
        y_from,
        x_to,
        y_to,
    }: PxBoundingBox,
) -> impl ExactSizeIterator<Item = [usize; 2]> {
    struct IterWithLen<I>(I, usize);
    impl<I: Iterator> ExactSizeIterator for IterWithLen<I> {}
    impl<I: Iterator> Iterator for IterWithLen<I> {
        type Item = I::Item;

        fn size_hint(&self) -> (usize, Option<usize>) {
            (self.1, Some(self.1))
        }

        fn next(&mut self) -> Option<Self::Item> {
            self.0.next()
        }
    }

    let len = (x_from..x_to).len() * (y_from..y_to).len();
    let iter = (x_from..x_to).flat_map(move |x| (y_from..y_to).map(move |y| [x, y]));

    debug_assert_eq!(len, iter.clone().count());

    IterWithLen(iter, len)
}

#[cfg(test)]
mod test {
    use egui::{Color32, Pos2, Vec2, emath::TSTransform, epaint::Vertex};

    use super::triangle_bounding_box;

    #[test]
    fn px_bounding_box() {
        let triangle = [
            Vertex {
                pos: Pos2::new(56.3, 18.9),
                uv: Default::default(),
                color: Color32::WHITE,
            },
            Vertex {
                pos: Pos2::new(56.4, 19.6),
                uv: Default::default(),
                color: Color32::WHITE,
            },
            Vertex {
                pos: Pos2::new(55.8, 20.5),
                uv: Default::default(),
                color: Color32::WHITE,
            },
        ];

        let pixels_per_point = 2.0;
        let point_to_pixel = TSTransform {
            scaling: pixels_per_point,
            translation: Vec2::new(-55.8, -18.9) * pixels_per_point,
        };

        let bounding_box = triangle_bounding_box(&triangle.each_ref(), point_to_pixel);

        insta::assert_debug_snapshot!((triangle, point_to_pixel, bounding_box));
    }
}

impl BlendFn for blend::Normal {
    fn blend(a: Color32, b: Color32) -> Color32 {
        a.blend(b)
    }
}

impl BlendFn for blend::Add {
    fn blend(a: Color32, b: Color32) -> Color32 {
        a + b
    }
}

impl BlendFn for blend::Multiply {
    fn blend(a: Color32, b: Color32) -> Color32 {
        a * b
    }
}
