use crate::{Color, Size};
use grid::Grid;
use std::collections::HashSet;

#[derive(Clone, Debug)]
pub enum Error {
    InvalidPosition,
}

#[derive(Clone, Debug)]
pub struct Board {
    pub size: Size,
    pub grid: Grid<Option<Color>>,
    pub groups: Vec<Group>,
}

impl std::fmt::Display for Board {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        print!("   ");
        // for c in 'A'..'U' {
        for c in 0..19 {
            print!("{:2}", c);
        }
        println!("");

        for row in 0..self.size.height {
            print!(" {:2}", row);
            for column in 0..self.size.width {
                match self.stone(&Coordinate { column, row }) {
                    None => write!(f, " .")?,
                    Some(Color::Black) => write!(f, " X")?,
                    Some(Color::White) => write!(f, " O")?,
                }
            }
            writeln!(f, "")?;
        }
        Ok(())
    }
}

impl Board {
    pub fn new() -> Self {
        Self {
            size: Size {
                width: 19,
                height: 19,
            },
            grid: Grid::new(19, 19),
            groups: Vec::new(),
        }
    }

    pub fn from_coordinates(coordinates: impl Iterator<Item = (Coordinate, Color)>) -> Self {
        let mut s = Self::new();
        for (coordinate, color) in coordinates {
            s.place_stone(coordinate, color);
        }
        s
    }
}

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

impl Board {
    pub fn place_stone(&mut self, coordinate: Coordinate, color: Color) -> Result<(), Error> {
        if let Some(_) = self.stone(&coordinate) {
            return Err(Error::InvalidPosition);
        }

        let mut friendly_group = self
            .adjacencies(&coordinate)
            .into_iter()
            .filter(|c| self.stone(c) == Some(color))
            .filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
            .fold(HashSet::new(), |acc, set| {
                acc.union(&set).cloned().collect()
            });

        friendly_group.insert(coordinate.clone());

        self.groups
            .retain(|g| g.coordinates.is_disjoint(&friendly_group));
        let friendly_group = Group {
            color,
            coordinates: friendly_group,
        };
        self.groups.push(friendly_group.clone());

        match self.grid.get_mut(coordinate.row, coordinate.column) {
            None => return Err(Error::InvalidPosition),
            Some(space) => *space = Some(color),
        }

        let adjacent_groups = self.adjacent_groups(&friendly_group);
        for group in adjacent_groups {
            if self.liberties(&group) == 0 {
                self.remove_group(&group);
            }
        }

        Ok(())
    }

    pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
        match self.grid.get(coordinate.row, coordinate.column) {
            None => None,
            Some(val) => *val,
        }
    }

    pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
        self.groups
            .iter()
            .find(|g| g.coordinates.contains(coordinate))
    }

    pub fn remove_group(&mut self, group: &Group) {
        self.groups.retain(|g| g != group);
        for coord in group.coordinates.iter() {
            match self.grid.get_mut(coord.row, coord.column) {
                None => (),
                Some(v) => *v = None,
            }
        }
    }

    pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
        let adjacent_spaces = self.group_halo(group).into_iter();
        println!("adjacent spaces: {:?}", adjacent_spaces);
        let mut grps: Vec<Group> = Vec::new();

        adjacent_spaces.for_each(|coord| match self.group(&coord) {
            None => return,
            Some(adj) => {
                println!("group found: {:?}", adj.color);
                if group.color == adj.color {
                    return;
                }
                if grps.iter().any(|g| g.coordinates.contains(&coord)) {
                    return;
                }
                grps.push(adj.clone());
            }
        });

        grps
    }

    pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
        group
            .coordinates
            .iter()
            .map(|c| self.adjacencies(c))
            .flatten()
            .collect::<HashSet<Coordinate>>()
    }

    pub fn liberties(&self, group: &Group) -> usize {
        self.group_halo(group)
            .into_iter()
            .filter(|c| self.stone(&c) == None)
            .count()

        // let adjacencies: HashSet<Coordinate> = self.adjacencies(group.coordinates.iter()).collect();
        /*
        println!("adjacencies: {:?}", adjacencies);
        let opposing_spaces = adjacencies
            .iter()
            .filter(|coord| match self.grid.get(coord.row, coord.column) {
                None => true,
                Some(&None) => false,
                Some(&Some(c)) => c != group.color,
            })
            .cloned()
            .collect::<HashSet<Coordinate>>();
        println!("opposition: {:?}", opposing_spaces);

        adjacencies.len() - opposing_spaces.len()
        */

        /*
        group
            .adjacencies(self.size.width as u8 - 1, self.size.height as u8 - 1)
            .into_iter()
            .filter(|coordinate| self.stone(*coordinate).is_none())
            .collect::<Vec<Coordinate>>()
            .len()
            */
    }

    pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
        let mut v = Vec::new();
        if coordinate.column > 0 {
            v.push(Coordinate {
                column: coordinate.column - 1,
                row: coordinate.row,
            });
        }
        if coordinate.row > 0 {
            v.push(Coordinate {
                column: coordinate.column,
                row: coordinate.row - 1,
            });
        }
        v.push(Coordinate {
            column: coordinate.column + 1,
            row: coordinate.row,
        });
        v.push(Coordinate {
            column: coordinate.column,
            row: coordinate.row + 1,
        });
        v.into_iter().filter(|c| self.within_board(c)).collect()
    }

    /*
    pub fn adjacencies<'a>(
        &'a self,
        coordinates: impl Iterator<Item = &'a Coordinate> + 'a,
    ) -> impl Iterator<Item = Coordinate> + 'a {
        coordinates
            .map(|coordinate| {
                let mut v = Vec::new();
                if coordinate.column > 0 {
                    v.push(Coordinate {
                        column: coordinate.column - 1,
                        row: coordinate.row,
                    });
                }
                if coordinate.row > 0 {
                    v.push(Coordinate {
                        column: coordinate.column,
                        row: coordinate.row - 1,
                    });
                }
                v.push(Coordinate {
                    column: coordinate.column + 1,
                    row: coordinate.row,
                });
                v.push(Coordinate {
                    column: coordinate.column,
                    row: coordinate.row + 1,
                });
                v
            })
            .flatten()
            .filter(|coordinate| self.within_board(coordinate))
    }
    */

    pub fn within_board(&self, coordinate: &Coordinate) -> bool {
        coordinate.column < self.size.width && coordinate.row < self.size.height
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Group {
    color: Color,
    coordinates: HashSet<Coordinate>,
}

/*
impl Group {
    pub fn adjacencies(&self, max_column: usize, max_row: usize) -> HashSet<Coordinate> {
        self.coordinates
            .iter()
            .map(|stone| stone.adjacencies(max_column, max_row))
            .flatten()
            .collect()
    }
}
*/

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

    /* Two players (Black and White) take turns and Black plays first
     * Stones are placed on the line intersections and not moved.
     * A stone with no liberties is removed from the board.
     * A group of stones of the same color share liberties.
     * A stone at the edge of the board has only three liberties.
     * A stone at the corner of the board has only two liberties.
     * A stone may not be placed in a suicidal position.
     * A stone placed in a suicidal position is legal if it captures other stones first.
     */

    fn with_example_board(test: impl FnOnce(Board)) {
        let board = Board::from_coordinates(
            vec![
                (Coordinate { column: 3, row: 3 }, Color::White),
                (Coordinate { column: 3, row: 4 }, Color::White),
                /* */
                (Coordinate { column: 8, row: 3 }, Color::Black),
                (Coordinate { column: 9, row: 3 }, Color::Black),
                (Coordinate { column: 9, row: 4 }, Color::Black),
                /* */
                (Coordinate { column: 15, row: 3 }, Color::White),
                (Coordinate { column: 15, row: 4 }, Color::White),
                (Coordinate { column: 15, row: 5 }, Color::White),
                (Coordinate { column: 14, row: 4 }, Color::White),
                /* */
                (Coordinate { column: 3, row: 8 }, Color::White),
                (Coordinate { column: 3, row: 9 }, Color::White),
                (Coordinate { column: 4, row: 9 }, Color::White),
                (Coordinate { column: 3, row: 10 }, Color::Black),
                /* */
                (Coordinate { column: 0, row: 0 }, Color::White),
                (Coordinate { column: 1, row: 0 }, Color::White),
                (Coordinate { column: 0, row: 1 }, Color::White),
                /* */
                (Coordinate { column: 9, row: 9 }, Color::White),
                (Coordinate { column: 8, row: 9 }, Color::Black),
                (Coordinate { column: 9, row: 8 }, Color::Black),
                (Coordinate { column: 9, row: 10 }, Color::Black),
                /* */
                (Coordinate { column: 0, row: 17 }, Color::White),
                (Coordinate { column: 1, row: 18 }, Color::White),
                (Coordinate { column: 0, row: 18 }, Color::White),
                (Coordinate { column: 0, row: 16 }, Color::Black),
                (Coordinate { column: 1, row: 17 }, Color::Black),
                /* */
                (Coordinate { column: 4, row: 17 }, Color::Black),
                (Coordinate { column: 5, row: 17 }, Color::Black),
                (Coordinate { column: 6, row: 17 }, Color::Black),
                (Coordinate { column: 4, row: 18 }, Color::Black),
                (Coordinate { column: 6, row: 18 }, Color::Black),
                (Coordinate { column: 3, row: 17 }, Color::White),
                (Coordinate { column: 3, row: 18 }, Color::White),
                (Coordinate { column: 4, row: 16 }, Color::White),
                (Coordinate { column: 5, row: 16 }, Color::White),
                (Coordinate { column: 6, row: 16 }, Color::White),
                (Coordinate { column: 7, row: 17 }, Color::White),
                (Coordinate { column: 7, row: 18 }, Color::White),
            ]
            .into_iter(),
        );
        test(board);
    }

    #[test]
    fn it_gets_adjacencies_for_coordinate() {
        let board = Board::new();
        for column in 0..19 {
            for row in 0..19 {
                for coordinate in board.adjacencies(&Coordinate { column, row }) {
                    assert!(
                        board.within_board(&coordinate),
                        "{} {}: {:?}",
                        column,
                        row,
                        coordinate
                    );
                }
            }
        }
    }

    #[test]
    fn it_counts_individual_liberties() {
        let board = Board::from_coordinates(
            vec![
                (Coordinate { column: 3, row: 3 }, Color::White),
                (Coordinate { column: 0, row: 3 }, Color::White),
                (Coordinate { column: 0, row: 0 }, Color::White),
                (Coordinate { column: 18, row: 9 }, Color::Black),
                (
                    Coordinate {
                        column: 18,
                        row: 18,
                    },
                    Color::Black,
                ),
            ]
            .into_iter(),
        );
        assert!(board.group(&Coordinate { column: 18, row: 3 }).is_none());
        assert_eq!(
            board
                .group(&Coordinate { column: 3, row: 3 })
                .map(|g| board.liberties(&g)),
            Some(4)
        );
        assert_eq!(
            board
                .group(&Coordinate { column: 0, row: 3 })
                .map(|g| board.liberties(&g)),
            Some(3)
        );
        assert_eq!(
            board
                .group(&Coordinate { column: 0, row: 0 })
                .map(|g| board.liberties(&g)),
            Some(2)
        );
        assert_eq!(
            board
                .group(&Coordinate { column: 18, row: 9 })
                .map(|g| board.liberties(&g)),
            Some(3)
        );
        assert_eq!(
            board
                .group(&Coordinate {
                    column: 18,
                    row: 18
                })
                .map(|g| board.liberties(&g)),
            Some(2)
        );
    }

    #[test]
    fn stones_share_liberties() {
        with_example_board(|board: Board| {
            let test_cases = vec![
                (
                    board.clone(),
                    Coordinate { column: 0, row: 0 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 0, row: 0 },
                            Coordinate { column: 1, row: 0 },
                            Coordinate { column: 0, row: 1 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(3),
                ),
                (
                    board.clone(),
                    Coordinate { column: 1, row: 0 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 0, row: 0 },
                            Coordinate { column: 1, row: 0 },
                            Coordinate { column: 0, row: 1 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(3),
                ),
                (
                    board.clone(),
                    Coordinate { column: 9, row: 9 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![Coordinate { column: 9, row: 9 }].into_iter().collect(),
                    }),
                    Some(1),
                ),
                (
                    board.clone(),
                    Coordinate { column: 3, row: 4 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 3, row: 3 },
                            Coordinate { column: 3, row: 4 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(6),
                ),
                (
                    board.clone(),
                    Coordinate { column: 9, row: 3 },
                    Some(Group {
                        color: Color::Black,
                        coordinates: vec![
                            Coordinate { column: 8, row: 3 },
                            Coordinate { column: 9, row: 3 },
                            Coordinate { column: 9, row: 4 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(7),
                ),
                (
                    board.clone(),
                    Coordinate { column: 15, row: 4 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 15, row: 3 },
                            Coordinate { column: 15, row: 4 },
                            Coordinate { column: 15, row: 5 },
                            Coordinate { column: 14, row: 4 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(8),
                ),
                (
                    board.clone(),
                    Coordinate { column: 3, row: 9 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 3, row: 8 },
                            Coordinate { column: 3, row: 9 },
                            Coordinate { column: 4, row: 9 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(6),
                ),
                (
                    board.clone(),
                    Coordinate { column: 0, row: 18 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 0, row: 17 },
                            Coordinate { column: 0, row: 18 },
                            Coordinate { column: 1, row: 18 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(1),
                ),
                (
                    board.clone(),
                    Coordinate { column: 0, row: 17 },
                    Some(Group {
                        color: Color::White,
                        coordinates: vec![
                            Coordinate { column: 0, row: 17 },
                            Coordinate { column: 0, row: 18 },
                            Coordinate { column: 1, row: 18 },
                        ]
                        .into_iter()
                        .collect(),
                    }),
                    Some(1),
                ),
            ];

            println!("{}", board);
            for (board, coordinate, group, liberties) in test_cases {
                assert_eq!(board.group(&coordinate), group.as_ref());
                assert_eq!(
                    board.group(&coordinate).map(|g| board.liberties(&g)),
                    liberties,
                    "{:?}",
                    coordinate
                );
            }
        });
    }

    #[test]
    fn it_finds_adjacent_groups() {
        with_example_board(|mut board| {
            println!("{}", board);
            let group = board
                .group(&Coordinate { column: 0, row: 0 })
                .cloned()
                .unwrap();
            assert_eq!(board.adjacent_groups(&group), Vec::new());

            let group = board
                .group(&Coordinate { column: 3, row: 10 })
                .cloned()
                .unwrap();
            assert_eq!(board.adjacent_groups(&group).len(), 1);
        });
    }

    #[test]
    fn surrounding_a_group_removes_it() {
        with_example_board(|mut board| {
            board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black);
            assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none());

            board.place_stone(Coordinate { column: 2, row: 18 }, Color::Black);
            assert!(board.stone(&Coordinate { column: 0, row: 18 }).is_none());
            assert!(board.stone(&Coordinate { column: 1, row: 18 }).is_none());
            assert!(board.stone(&Coordinate { column: 0, row: 17 }).is_none());
            assert!(board.group(&Coordinate { column: 0, row: 18 }).is_none());

            board.place_stone(Coordinate { column: 5, row: 18 }, Color::White);
            assert!(board.stone(&Coordinate { column: 4, row: 17 }).is_none());
            assert!(board.stone(&Coordinate { column: 5, row: 17 }).is_none());
            assert!(board.stone(&Coordinate { column: 6, row: 17 }).is_none());
            assert!(board.stone(&Coordinate { column: 4, row: 18 }).is_none());
            assert!(board.stone(&Coordinate { column: 6, row: 18 }).is_none());
        });
    }

    fn suicide_is_forbidden() {
        assert!(false);
    }

    fn captures_preceed_self_capture() {
        assert!(false);
    }
}