use bevy::prelude::*;

use rand::{rngs::ThreadRng, Rng};

use super::cell::Cell;

#[derive(Debug, Component)]
pub struct Board {
    pub size: (usize, usize),
    pub col_hints: Vec<Vec<usize>>,
    pub row_hints: Vec<Vec<usize>>,
    data: Vec<Cell>,
}

impl Board {
    #[allow(unused_variables)]
    pub fn new<const W: usize, const H: usize>(with_hints: bool) -> Self {
        let mut rng = ::rand::rng();
        let mut board = Board {
            size: (W, H),
            col_hints: vec![],  /* this vec is gonna be discarded */
            row_hints: vec![],  /* this vec is gonna be discarded */
            data: (0..(W*H)).map(|_| Cell::default()).collect(),
        };
        for (x, y) in Board::pairs(W, H) {
            board.data[y * W + x] = (rng.random_range(0..16u8) < 10u8).into();
        }
        board.reset_hints();
        #[cfg(not(test))]
        board.close_cells(W, H, with_hints, &mut rng);
        board
    }

    fn close_cells(&mut self, w: usize, h: usize , with_hints: bool, rng: &mut ThreadRng) {
        for (x, y) in Board::pairs(w, h) {
            if !with_hints || rng.random_range(0..16u8) < 10u8 {
                self.data[y * w + x] = Cell::Closed;
            } else {
                self.data[y * w + x] = self.data[y * w + x].lock();
            }
        }
    }

    pub fn get(&self, x: usize, y: usize) -> Cell {
        let (w, h) = self.size;
        let idx = (x % w) + (y % h) * w;
        self.data[idx]
    }

    pub fn set(&mut self, x: usize, y: usize, value: Cell) -> bool {
        let (w, h) = self.size;
        let idx = (x % w) + (y % h) * w;

        if self.data[idx] == value {
            return false;
        }

        match self.data[idx] {
            Cell::Yes if value != Cell::Closed => false,
            Cell::No if value != Cell::Closed => false,
            Cell::LockedYes |
            Cell::LockedNo => false,
            _ => {
                self.data[idx] = value;
                true
            }
        }
    }

    pub fn is_done(&self) -> bool {
        let (w, h) = self.size;
        let (hhints, vhints) = self.calculate();
        for x in 0..w {
            if hhints[x] != self.col_hints[x] {
                return false;
            }
        }
        for y in 0..h {
            if vhints[y] != self.row_hints[y] {
                return false;
            }
        }
        true
    }

    fn reset_hints(&mut self) {
        let (col_hints, row_hints) = self.calculate();
        self.col_hints = col_hints;
        self.row_hints = row_hints;
    }

    #[inline]
    fn calculate(&self) -> (Vec<Vec<usize>>, Vec<Vec<usize>>) {
        (self.calculate_col_hints(), self.calculate_row_hints())
    }

    fn calculate_col_hints(&self) -> Vec<Vec<usize>> {
        let (w, h) = self.size;
        let mut col_hints: Vec<Vec<usize>> = (0..w).map(|_| Vec::new()).collect();
        for x in 0..w {
            let mut last = false;
            let mut count = 0_usize;
            for y in 0..h {
                if !last && count > 0 {
                    col_hints[x].push(count);
                    count = 0;
                }
                if self.get(x, y).into() {
                    last = true;
                    count += 1;
                } else {
                    last = false;
                }
            }
            if count > 0 {
                col_hints[x].push(count);
            }
        }
        col_hints
    }

    fn calculate_row_hints(&self) -> Vec<Vec<usize>> {
        let (w, h) = self.size;
        let mut row_hints: Vec<Vec<usize>> = (0..h).map(|_| Vec::new()).collect();
        for y in 0..h {
            let mut last = false;
            let mut count = 0_usize;
            for x in 0..w {
                if !last && count > 0 {
                    row_hints[y].push(count);
                    count = 0;
                }
                if self.get(x, y).into() {
                    last = true;
                    count += 1;
                } else {
                    last = false;
                }
            }
            if count > 0 {
                row_hints[y].push(count);
            }
        }
        row_hints
    }

    #[inline]
    fn pairs(w: usize, h: usize) -> impl Iterator<Item = (usize, usize)> {
        (0..h).flat_map(move |y| (0..w).map(move |x| (x, y)))
    }
}

impl ToString for Board {

    fn to_string(&self) -> String {
        let (w, h) = self.size;
        let mut res = String::new();

        let count = self.col_hints
            .iter()
            .map(|e| e.len())
            .max()
            .unwrap_or(0);
        let mut lines: Vec<String> = Vec::with_capacity(count);
        (0..count).for_each(|_| lines.push("".to_string()));
        for x in 0..w {
            for i in 0..count {
                let cur = self.col_hints[x]
                    .get(i)
                    .map(|e| format!("{e:X}"))
                    .unwrap_or(" ".to_owned());
                lines[i] += &cur;
            }
        }
        for line in &lines {
            res += line;
            res += "\n";
        }
        for y in 0..h {
            for x in 0..w {
                res += &self.get(x, y).to_string();
            }
            for value in self.row_hints[y].iter() {
                res += &format!("{value:X}");
            }
            res += "\n";
        }
        res
    }
}

impl Clone for Board {

    fn clone(&self) -> Self {
        Self {
            size: self.size.clone(),
            col_hints: self.col_hints.clone(),
            row_hints: self.row_hints.clone(),
            data: self.data.iter().map(|cell| cell.clone()).collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::LevelSize;

    #[test]
    fn new_board() {
        let board = Board::new::<10, 5>(false);
        assert!(board.is_done());
    }

    #[test]
    fn close_cells() {
        let mut rng = ::rand::rng();
        let mut board = Board::new::<10, 5>(false);
        board.close_cells(10, 5, false, &mut rng);
        assert!(!board.is_done());
    }

    #[test]
    fn solve() {
        let mut rng = ::rand::rng();
        for _ in 0..10 {
            for size in [
                LevelSize::Size5x5,
                LevelSize::Size10x5,
                LevelSize::Size10x10,
                LevelSize::Size15x10,
                LevelSize::Size15x15,
            ] {
                let original = size.board(false);
                let mut board = original.clone();
                let (w, h) = board.size.clone();
                board.close_cells(w, h, false, &mut rng);
                assert!(original.is_done());
                assert!(!board.is_done());

                for y in 0..h {
                    for x in 0..w {
                        let must_set: bool = original.get(x, y).into();
                        if must_set {
                            board.set(x, y, Cell::Yes);
                        } else {
                            board.set(x, y, Cell::No);
                        }
                    }
                }
                assert!(board.is_done());
            }
        }
    }

    #[test]
    fn set() {
        let mut rng = ::rand::rng();
        let mut board = Board::new::<5, 5>(false);
        board.close_cells(5, 5, false, &mut rng);

        for y in 0..5 {
            for x in 0..5 {
                assert!(!board.set(x, y, Cell::Closed));

                assert!(board.set(x, y, Cell::Yes));
                assert!(!board.set(x, y, Cell::Yes));
                assert!(!board.set(x, y, Cell::No));
                assert!(!board.set(x, y, Cell::MarkedClosed));
                assert!(board.set(x, y, Cell::Closed));

                assert!(board.set(x, y, Cell::No));
                assert!(!board.set(x, y, Cell::Yes));
                assert!(!board.set(x, y, Cell::No));
                assert!(!board.set(x, y, Cell::MarkedClosed));
                assert!(board.set(x, y, Cell::Closed));

                assert!(board.set(x, y, Cell::MarkedClosed));
                assert!(board.set(x, y, Cell::Yes));
                assert!(board.set(x, y, Cell::Closed));
                assert!(board.set(x, y, Cell::MarkedClosed));
                assert!(board.set(x, y, Cell::No));
                assert!(board.set(x, y, Cell::Closed));
                assert!(board.set(x, y, Cell::MarkedClosed));
                assert!(board.set(x, y, Cell::Closed));

                assert!(board.set(x, y, if (x+y) % 2 == 0 { Cell::LockedYes} else { Cell::LockedNo }));
                assert!(!board.set(x, y, Cell::Yes));
                assert!(!board.set(x, y, Cell::No));
                assert!(!board.set(x, y, Cell::MarkedClosed));
                assert!(!board.set(x, y, Cell::Closed));
            }
        }
    }
}
