use bone_types::{IconId, IconTile};

pub const TILE_PAD: u32 = 2;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct GridCell {
    pub col: usize,
    pub row: usize,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct AtlasGrid {
    cols: usize,
    rows: usize,
}

impl AtlasGrid {
    pub const DEFAULT: Self = Self { cols: 8, rows: 8 };

    #[must_use]
    pub const fn new(cols: usize, rows: usize) -> Self {
        Self { cols, rows }
    }

    #[must_use]
    pub const fn cols(self) -> usize {
        self.cols
    }

    #[must_use]
    pub const fn rows(self) -> usize {
        self.rows
    }

    #[must_use]
    pub const fn capacity(self) -> usize {
        self.cols * self.rows
    }

    #[must_use]
    pub fn cell(self, tile: IconTile) -> GridCell {
        let index = tile.as_usize();
        GridCell {
            col: index % self.cols,
            row: index / self.cols,
        }
    }

    #[must_use]
    pub fn pixel_size(self, page: AtlasPage) -> (u32, u32) {
        let cell = page.cell_px();
        let Ok(cols) = u32::try_from(self.cols) else {
            panic!("atlas grid columns fit a u32");
        };
        let Ok(rows) = u32::try_from(self.rows) else {
            panic!("atlas grid rows fit a u32");
        };
        (cols * cell, rows * cell)
    }

    #[must_use]
    pub fn tile_origin(self, cell: GridCell, page: AtlasPage) -> (u32, u32) {
        let stride = page.cell_px();
        let Ok(col) = u32::try_from(cell.col) else {
            panic!("atlas cell column fits a u32");
        };
        let Ok(row) = u32::try_from(cell.row) else {
            panic!("atlas cell row fits a u32");
        };
        (col * stride + TILE_PAD, row * stride + TILE_PAD)
    }
}

const _: () = assert!(
    IconId::COUNT <= AtlasGrid::DEFAULT.capacity(),
    "icon count exceeds the default atlas grid; grow AtlasGrid::DEFAULT",
);

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum AtlasPage {
    Px16,
    Px24,
    Px32,
    Px48,
}

impl AtlasPage {
    pub const ALL: [AtlasPage; 4] = [
        AtlasPage::Px16,
        AtlasPage::Px24,
        AtlasPage::Px32,
        AtlasPage::Px48,
    ];

    #[must_use]
    pub const fn side_px(self) -> u32 {
        match self {
            AtlasPage::Px16 => 16,
            AtlasPage::Px24 => 24,
            AtlasPage::Px32 => 32,
            AtlasPage::Px48 => 48,
        }
    }

    #[must_use]
    pub const fn cell_px(self) -> u32 {
        self.side_px() + 2 * TILE_PAD
    }

    #[must_use]
    pub fn nearest(requested_px: u32) -> AtlasPage {
        AtlasPage::ALL
            .into_iter()
            .filter(|page| page.side_px() >= requested_px)
            .min_by_key(|page| page.side_px())
            .unwrap_or(AtlasPage::Px48)
    }
}

const _: () = assert!(
    AtlasPage::Px16.side_px() < AtlasPage::Px24.side_px()
        && AtlasPage::Px24.side_px() < AtlasPage::Px32.side_px()
        && AtlasPage::Px32.side_px() < AtlasPage::Px48.side_px(),
    "AtlasPage sizes must stay distinct and ascending so Px48 is the largest fallback for nearest",
);

#[cfg(test)]
mod tests {
    use super::{AtlasGrid, AtlasPage, GridCell};
    use bone_types::IconId;

    #[test]
    fn default_grid_holds_every_icon() {
        assert_eq!(AtlasGrid::DEFAULT.capacity(), 64);
        assert!(IconId::COUNT <= AtlasGrid::DEFAULT.capacity());
    }

    #[test]
    fn cell_is_row_major() {
        let grid = AtlasGrid::DEFAULT;
        assert_eq!(grid.cell(IconId::Point.tile()), GridCell { col: 0, row: 0 });
        assert_eq!(
            grid.cell(IconId::PerimeterCircle.tile()),
            GridCell { col: 6, row: 0 }
        );
        assert_eq!(
            grid.cell(IconId::ThreePointCornerRectangle.tile()),
            GridCell { col: 1, row: 1 }
        );
        assert_eq!(
            grid.cell(IconId::ThreePointCenterRectangle.tile()),
            GridCell { col: 2, row: 1 }
        );
    }

    #[test]
    fn every_icon_cell_is_within_the_grid() {
        let grid = AtlasGrid::DEFAULT;
        IconId::ALL.iter().for_each(|icon| {
            let cell = grid.cell(icon.tile());
            assert!(cell.col < grid.cols(), "{icon:?} column out of range");
            assert!(cell.row < grid.rows(), "{icon:?} row out of range");
        });
    }

    #[test]
    fn nearest_page_picks_the_smallest_sufficient_size() {
        assert_eq!(AtlasPage::nearest(14), AtlasPage::Px16);
        assert_eq!(AtlasPage::nearest(16), AtlasPage::Px16);
        assert_eq!(AtlasPage::nearest(17), AtlasPage::Px24);
        assert_eq!(AtlasPage::nearest(48), AtlasPage::Px48);
        assert_eq!(AtlasPage::nearest(1000), AtlasPage::Px48);
    }

    #[test]
    fn page_sides_match_logical_sizes() {
        assert_eq!(AtlasPage::Px16.side_px(), 16);
        assert_eq!(AtlasPage::Px24.side_px(), 24);
        assert_eq!(AtlasPage::Px32.side_px(), 32);
        assert_eq!(AtlasPage::Px48.side_px(), 48);
    }
}
