use crate::bitboard::{Bitboard, ls1b, pop_count};

static NOT_A_FILE: Bitboard = 0xFEFEFEFEFEFEFEFE;
static NOT_B_FILE: Bitboard = 0xFDFDFDFDFDFDFDFD;
static NOT_G_FILE: Bitboard = 0xBFBFBFBFBFBFBFBF;
static NOT_H_FILE: Bitboard = 0x7F7F7F7F7F7F7F7F;

/// An array where N-th item is an attack bitboard
/// of a piece on N-th square
type AttackTable = [Bitboard; 64];

/// One byte stores exactly 256 states which is every
/// possible state of the occupancy on a rank
pub type Byte = u8;

/// First index is *occupancy* of the first rank (all possible states)
/// and the second index is position of the piece on the first rank
/// ```
/// rank_attacks = first_rank_attacks[occupancy][piece_file];
/// ```
type FirstRankAttacks = [[Byte; 8]; 256];


#[derive(Debug)]
pub struct Attacks {
    pub knight: AttackTable,
    pub king: AttackTable,
    pub first_rank_attacks: FirstRankAttacks,
}

impl Attacks {
    pub fn new() -> Self {
        let mut knight = [0; 64];
        let mut king = [0; 64];
        let mut first_rank_attacks = [[0; 8]; 256];
        Self::precompute_knight_attacks(&mut knight);
        Self::precompute_king_attacks(&mut king);
        Self::precompute_first_rank_attacks(&mut first_rank_attacks);
        Self { knight, king, first_rank_attacks }
    }

    fn precompute_knight_attacks(attacks: &mut AttackTable) {
        for index in 0..64 {
            let position = 1u64 << index;
            attacks[index] =
                ((position & NOT_A_FILE & NOT_B_FILE) << 6) |
                ((position & NOT_G_FILE & NOT_H_FILE) << 10) |
                ((position & NOT_A_FILE) << 15) |
                ((position & NOT_H_FILE) << 17) |
                ((position & NOT_G_FILE & NOT_H_FILE) >> 6) |
                ((position & NOT_A_FILE & NOT_B_FILE) >> 10) |
                ((position & NOT_H_FILE) >> 15) |
                ((position & NOT_A_FILE) >> 17);
        }
    }

    fn precompute_king_attacks(attacks: &mut AttackTable) {
        for index in 0..64 {
            let position = 1u64 << index;
            attacks[index] =
                ((position & NOT_A_FILE) >> 1) |
                ((position & NOT_A_FILE) << 7) |
                ((position & NOT_A_FILE) >> 9) |
                ((position & NOT_H_FILE) << 1) |
                ((position & NOT_H_FILE) >> 7) |
                ((position & NOT_H_FILE) << 9) |
                (position << 8) |
                (position >> 8);
        }
    }

    fn precompute_first_rank_attacks(attacks: &mut FirstRankAttacks) {
        /// Really slow implementation of Most Significant One Bit which is fine to use when pre-computing
        fn log_base_2(bb: Bitboard) -> u64 {
            (bb as f64).log2() as u64
        }

        for occupancy in 0..256 {
            let right_most_piece = log_base_2(occupancy);
            let left_most_piece = log_base_2(ls1b(occupancy));

            for file in 0..8 {
                let left_bound = if left_most_piece == file {
                    0
                } else {
                    left_most_piece
                };

                let right_bound = if right_most_piece == file {
                    7
                } else {
                    right_most_piece
                };

                for index in left_bound..right_bound+1 {
                    if index != file {
                        attacks[occupancy as usize][file as usize] |= 1 << index;
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{bitboard::pop_count, board::Square};

    use super::*;

    #[test]
    fn test_knight_attacks() {
        let attacks = Attacks::new();
        let e4_attacks = attacks.knight[Square::E4 as usize];

        assert_ne!(e4_attacks & 1 << Square::G5 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::G3 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::C5 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::C3 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::D2 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::F2 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::D6 as usize, 0);
        assert_ne!(e4_attacks & 1 << Square::F6 as usize, 0);

        assert_eq!(e4_attacks & 1 << Square::E5 as usize, 0);
        assert_eq!(e4_attacks & 1 << Square::D4 as usize, 0);
        assert_eq!(e4_attacks & 1 << Square::A1 as usize, 0);

        assert_eq!(pop_count(attacks.knight[Square::G1 as usize]), 3);
        assert_eq!(pop_count(attacks.knight[Square::H8 as usize]), 2);
    }

    #[test]
    fn test_king_attacks() {
        let attacks = Attacks::new();
        assert_eq!(pop_count(attacks.king[Square::E4 as usize]), 8);

        assert_eq!(pop_count(attacks.king[Square::A1 as usize]), 3);
        assert_eq!(pop_count(attacks.king[Square::A8 as usize]), 3);
        assert_eq!(pop_count(attacks.king[Square::H1 as usize]), 3);
        assert_eq!(pop_count(attacks.king[Square::H8 as usize]), 3);

        assert_eq!(pop_count(attacks.king[Square::E1 as usize]), 5);
        assert_eq!(pop_count(attacks.king[Square::E8 as usize]), 5);
        assert_eq!(pop_count(attacks.king[Square::A4 as usize]), 5);
        assert_eq!(pop_count(attacks.king[Square::H4 as usize]), 5);
    }

    #[test]
    fn test_first_rank_attacks() {
        let attacks = Attacks::new();
        //                                      HGFEDCBA        HGFEDCBA
        assert_eq!(attacks.first_rank_attacks[0b00010000][4], 0b11101111, "If rook is the only one on the file, it should be able to attack all rank");
        assert_eq!(attacks.first_rank_attacks[0b00010001][4], 0b11101111, "If only other piece is on A rank, rook should be able to attack all rank");
        assert_eq!(attacks.first_rank_attacks[0b10010000][4], 0b11101111, "If only other piece is on H rank, rook should be able to attack all rank");
        assert_eq!(attacks.first_rank_attacks[0b10010001][4], 0b11101111, "If only other pieces are on A and H ranks, rook should be able to attack all rank");
        assert_eq!(attacks.first_rank_attacks[0b00010100][4], 0b11101100);
        assert_eq!(attacks.first_rank_attacks[0b01010100][4], 0b01101100);
        assert_eq!(attacks.first_rank_attacks[0b01010010][4], 0b01101110);

    }
}