// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Primitives for creating tiles.

use crate::flatten::Line;
use std::vec;
use std::vec::Vec;

/// The max number of lines per path.
///
/// Trying to render a path with more lines than this may result in visual artifacts.
pub(crate) const MAX_LINES_PER_PATH: u32 = 1 << 31;

/// A tile represents an aligned area on the pixmap, used to subdivide the viewport into sub-areas
/// (currently 4x4) and analyze line intersections inside each such area.
///
/// Keep in mind that it is possible to have multiple tiles with the same index,
/// namely if we have multiple lines crossing the same 4x4 area!
///
/// # Note
///
/// This struct is `#[repr(C)]`, but the byte order of its fields is dependent on the endianness of
/// the compilation target.
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub(crate) struct Tile {
    // The field ordering is important.
    //
    // The given ordering (variant over little and big endian compilation targets), ensures that
    // `Tile::to_bits` doesn't do any actual work, as the ordering of the fields is such that the
    // numeric value of a `Tile` in memory is identical as returned by that method. This allows
    // for, e.g., comparison and sorting.
    #[cfg(target_endian = "big")]
    /// The index of the tile in the y direction.
    pub y: u16,

    #[cfg(target_endian = "big")]
    /// The index of the tile in the x direction.
    pub x: u16,

    /// The index of the line this tile belongs to into the line buffer, plus whether the line
    /// crosses the top edge of the tile, packed together.
    ///
    /// The index is the unsigned number in the 31 least significant bits of this value.
    ///
    /// The last bit is 1 if and only if the lines crosses the tile's top edge. Lines making this
    /// crossing increment or decrement the coarse tile winding, depending on the line direction.
    pub(crate) packed_winding_line_idx: u32,

    #[cfg(target_endian = "little")]
    /// The index of the tile in the x direction.
    pub(crate) x: u16,

    #[cfg(target_endian = "little")]
    /// The index of the tile in the y direction.
    pub(crate) y: u16,
}

impl Tile {
    /// The width of a tile in pixels.
    pub(crate) const WIDTH: u16 = 4;

    /// The height of a tile in pixels.
    pub(crate) const HEIGHT: u16 = 4;

    /// Create a new tile.
    ///
    /// `line_idx` must be smaller than [`MAX_LINES_PER_PATH`].
    #[inline]
    pub(crate) const fn new(x: u16, y: u16, line_idx: u32, winding: bool) -> Self {
        #[cfg(debug_assertions)]
        if line_idx >= MAX_LINES_PER_PATH {
            panic!("Max. number of lines per path exceeded.");
        }
        Self {
            x,
            y,
            packed_winding_line_idx: ((winding as u32) << 31) | line_idx,
        }
    }

    /// Check whether two tiles are at the same location.
    #[inline]
    pub(crate) const fn same_loc(&self, other: &Self) -> bool {
        self.same_row(other) && self.x == other.x
    }

    /// Check whether `self` is adjacent to the left of `other`.
    #[inline]
    pub(crate) const fn prev_loc(&self, other: &Self) -> bool {
        self.same_row(other) && self.x + 1 == other.x
    }

    /// Check whether two tiles are on the same row.
    #[inline]
    pub(crate) const fn same_row(&self, other: &Self) -> bool {
        self.y == other.y
    }

    /// The index of the line this tile belongs to into the line buffer.
    #[inline]
    pub(crate) const fn line_idx(&self) -> u32 {
        self.packed_winding_line_idx & (MAX_LINES_PER_PATH - 1)
    }

    /// Whether the line crosses the top edge of the tile.
    ///
    /// Lines making this crossing increment or decrement the coarse tile winding, depending on the
    /// line direction.
    #[inline]
    pub(crate) const fn winding(&self) -> bool {
        (self.packed_winding_line_idx & MAX_LINES_PER_PATH) != 0
    }

    /// Return the `u64` representation of this tile.
    ///
    /// This is the u64 interpretation of `(y, x, packed_winding_line_idx)` where `y` is the
    /// most-significant part of the number and `packed_winding_line_idx` the least significant.
    #[inline(always)]
    const fn to_bits(self) -> u64 {
        ((self.y as u64) << 48) | ((self.x as u64) << 32) | self.packed_winding_line_idx as u64
    }
}

impl PartialEq for Tile {
    #[inline(always)]
    fn eq(&self, other: &Self) -> bool {
        self.to_bits() == other.to_bits()
    }
}

impl Ord for Tile {
    #[inline(always)]
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        self.to_bits().cmp(&other.to_bits())
    }
}

impl PartialOrd for Tile {
    #[inline(always)]
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Eq for Tile {}

/// Handles the tiling of paths.
#[derive(Clone, Debug)]
pub(crate) struct Tiles {
    tile_buf: Vec<Tile>,
    sorted: bool,
}

impl Default for Tiles {
    fn default() -> Self {
        Self::new()
    }
}

impl Tiles {
    /// Create a new tiles container.
    pub(crate) fn new() -> Self {
        Self {
            tile_buf: vec![],
            sorted: false,
        }
    }

    /// Get the number of tiles in the container.
    pub(crate) fn len(&self) -> u32 {
        self.tile_buf.len() as u32
    }

    /// Returns `true` if the container has no tiles.
    pub(crate) fn is_empty(&self) -> bool {
        self.tile_buf.is_empty()
    }

    /// Reset the tiles' container.
    pub(crate) fn reset(&mut self) {
        self.tile_buf.clear();
        self.sorted = false;
    }

    /// Sort the tiles in the container.
    pub(crate) fn sort_tiles(&mut self) {
        self.sorted = true;
        self.tile_buf.sort_unstable();
    }

    /// Get the tile at a certain index.
    ///
    /// Panics if the container hasn't been sorted before.
    #[inline]
    pub(crate) fn get(&self, index: u32) -> &Tile {
        assert!(
            self.sorted,
            "attempted to call `get` before sorting the tile container."
        );

        &self.tile_buf[index as usize]
    }

    /// Iterate over the tiles in sorted order.
    ///
    /// Panics if the container hasn't been sorted before.
    #[inline]
    pub(crate) fn iter(&self) -> impl Iterator<Item = &Tile> {
        assert!(
            self.sorted,
            "attempted to call `iter` before sorting the tile container."
        );

        self.tile_buf.iter()
    }

    /// Populate the tiles' container with a buffer of lines.
    ///
    /// Tiles exceeding the top, right or bottom of the viewport (given by `width` and `height` in
    /// pixels) are culled.
    //
    // TODO: Tiles are clamped to the left edge of the viewport, but lines fully to the left of the
    // viewport are not culled yet. These lines impact winding, and would need forwarding of
    // winding to the strip generation stage.
    pub(crate) fn make_tiles(&mut self, lines: &[Line], width: u16, height: u16) {
        self.reset();

        if width == 0 || height == 0 {
            return;
        }

        debug_assert!(
            lines.len() <= MAX_LINES_PER_PATH as usize,
            "Max. number of lines per path exceeded. Max is {}, got {}.",
            MAX_LINES_PER_PATH,
            lines.len()
        );

        let tile_columns = width.div_ceil(Tile::WIDTH);
        let tile_rows = height.div_ceil(Tile::HEIGHT);

        for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
            let line_idx = line_idx as u32;

            let p0_x = line.p0.x / f32::from(Tile::WIDTH);
            let p0_y = line.p0.y / f32::from(Tile::HEIGHT);
            let p1_x = line.p1.x / f32::from(Tile::WIDTH);
            let p1_y = line.p1.y / f32::from(Tile::HEIGHT);

            let (line_left_x, line_right_x) = if p0_x < p1_x {
                (p0_x, p1_x)
            } else {
                (p1_x, p0_x)
            };
            let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
                (p0_y, p0_x, p1_y, p1_x)
            } else {
                (p1_y, p1_x, p0_y, p0_x)
            };

            // For ease of logic, special-case purely vertical tiles.
            if line_left_x == line_right_x {
                let y_top_tiles = (line_top_y as u16).min(tile_rows);
                let y_bottom_tiles = (line_bottom_y.ceil() as u16).min(tile_rows);

                let x = line_left_x as u16;
                for y_idx in y_top_tiles..y_bottom_tiles {
                    let y = f32::from(y_idx);

                    let tile = Tile::new(x, y_idx, line_idx, y >= line_top_y);
                    self.tile_buf.push(tile);
                }
            } else {
                let x_slope = (p1_x - p0_x) / (p1_y - p0_y);

                let y_top_tiles = (line_top_y as u16).min(tile_rows);
                let y_bottom_tiles = (line_bottom_y.ceil() as u16).min(tile_rows);

                for y_idx in y_top_tiles..y_bottom_tiles {
                    let y = f32::from(y_idx);

                    // The line's y-coordinates at the line's top- and bottom-most points within
                    // the tile row.
                    let line_row_top_y = line_top_y.max(y).min(y + 1.);
                    let line_row_bottom_y = line_bottom_y.max(y).min(y + 1.);

                    // The line's x-coordinates at the line's top- and bottom-most points within the
                    // tile row.
                    let line_row_top_x = p0_x + (line_row_top_y - p0_y) * x_slope;
                    let line_row_bottom_x = p0_x + (line_row_bottom_y - p0_y) * x_slope;

                    // The line's x-coordinates at the line's left- and right-most points within the
                    // tile row.
                    let line_row_left_x =
                        f32::min(line_row_top_x, line_row_bottom_x).max(line_left_x);
                    let line_row_right_x =
                        f32::max(line_row_top_x, line_row_bottom_x).min(line_right_x);

                    let winding_x = if line_top_x < line_bottom_x {
                        line_row_left_x as u16
                    } else {
                        line_row_right_x as u16
                    };

                    for x_idx in
                        line_row_left_x as u16..=(line_row_right_x as u16).min(tile_columns - 1)
                    {
                        let tile = Tile::new(
                            x_idx,
                            y_idx,
                            line_idx,
                            y >= line_top_y && x_idx == winding_x,
                        );
                        self.tile_buf.push(tile);
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::flatten::{Line, Point};
    use crate::tile::{Tile, Tiles};

    #[test]
    fn cull_line_at_top() {
        let line = Line {
            p0: Point { x: 3.0, y: -5.0 },
            p1: Point { x: 9.0, y: -1.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);

        assert!(tiles.is_empty());
    }

    #[test]
    fn cull_line_at_right() {
        let line = Line {
            p0: Point { x: 101.0, y: 0.0 },
            p1: Point { x: 103.0, y: 20.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);

        assert!(tiles.is_empty());
    }

    #[test]
    fn cull_line_at_bottom() {
        let line = Line {
            p0: Point { x: 30.0, y: 101.0 },
            p1: Point { x: 35.0, y: 105.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);

        assert!(tiles.is_empty());
    }

    #[test]
    fn partially_cull_line_exceeding_viewport() {
        let line = Line {
            p0: Point { x: -2.0, y: -3.0 },
            p1: Point { x: 2.0, y: 1.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);

        assert_eq!(tiles.tile_buf, [Tile::new(0, 0, 0, true)]);
    }

    #[test]
    fn horizontal_straight_line() {
        let line = Line {
            p0: Point { x: 1.5, y: 1.0 },
            p1: Point { x: 8.5, y: 1.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);
        tiles.sort_tiles();

        assert_eq!(
            tiles.tile_buf,
            [
                Tile::new(0, 0, 0, false),
                Tile::new(1, 0, 0, false),
                Tile::new(2, 0, 0, false),
            ]
        );
    }

    #[test]
    fn vertical_straight_line() {
        let line = Line {
            p0: Point { x: 1.0, y: 1.5 },
            p1: Point { x: 1.0, y: 8.5 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);
        tiles.sort_tiles();

        assert_eq!(
            tiles.tile_buf,
            [
                Tile::new(0, 0, 0, false),
                Tile::new(0, 1, 0, true),
                Tile::new(0, 2, 0, true),
            ]
        );
    }

    #[test]
    fn top_left_to_bottom_right() {
        let line = Line {
            p0: Point { x: 1.0, y: 1.0 },
            p1: Point { x: 11.0, y: 8.5 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);
        tiles.sort_tiles();

        assert_eq!(
            tiles.tile_buf,
            [
                Tile::new(0, 0, 0, false),
                Tile::new(1, 0, 0, false),
                Tile::new(1, 1, 0, true),
                Tile::new(2, 1, 0, false),
                Tile::new(2, 2, 0, true),
            ]
        );
    }

    #[test]
    fn bottom_right_to_top_left() {
        let line = Line {
            p0: Point { x: 11.0, y: 8.5 },
            p1: Point { x: 1.0, y: 1.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);
        tiles.sort_tiles();

        assert_eq!(
            tiles.tile_buf,
            [
                Tile::new(0, 0, 0, false),
                Tile::new(1, 0, 0, false),
                Tile::new(1, 1, 0, true),
                Tile::new(2, 1, 0, false),
                Tile::new(2, 2, 0, true),
            ]
        );
    }

    #[test]
    fn bottom_left_to_top_right() {
        let line = Line {
            p0: Point { x: 2.0, y: 11.0 },
            p1: Point { x: 14.0, y: 6.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);
        tiles.sort_tiles();

        assert_eq!(
            tiles.tile_buf,
            [
                Tile::new(2, 1, 0, false),
                Tile::new(3, 1, 0, false),
                Tile::new(0, 2, 0, false),
                Tile::new(1, 2, 0, false),
                Tile::new(2, 2, 0, true),
            ]
        );
    }

    #[test]
    fn top_right_to_bottom_left() {
        let line = Line {
            p0: Point { x: 14.0, y: 6.0 },
            p1: Point { x: 2.0, y: 11.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 100, 100);
        tiles.sort_tiles();

        assert_eq!(
            tiles.tile_buf,
            [
                Tile::new(2, 1, 0, false),
                Tile::new(3, 1, 0, false),
                Tile::new(0, 2, 0, false),
                Tile::new(1, 2, 0, false),
                Tile::new(2, 2, 0, true),
            ]
        );
    }

    #[test]
    fn two_lines_in_single_tile() {
        let line_1 = Line {
            p0: Point { x: 1.0, y: 3.0 },
            p1: Point { x: 3.0, y: 3.0 },
        };

        let line_2 = Line {
            p0: Point { x: 3.0, y: 3.0 },
            p1: Point { x: 0.0, y: 1.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line_1, line_2], 100, 100);

        assert_eq!(
            tiles.tile_buf,
            [Tile::new(0, 0, 0, false), Tile::new(0, 0, 1, false),]
        );
    }

    #[test]
    // See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46.
    fn infinite_loop() {
        let line = Line {
            p0: Point { x: 22.0, y: 552.0 },
            p1: Point { x: 224.0, y: 388.0 },
        };

        let mut tiles = Tiles::new();
        tiles.make_tiles(&[line], 600, 600);
    }
}
