/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of On the Grid.

On the Grid is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.

On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/

// TBQH, I don't recall what state this object is in, but I do know that I might have some troubles
// integrating it with a game record. Some of the time here is going to be me reading (and
// documenting) my code from almost a year ago.
//
use crate::{BoardError, Color, Size};
use sgf::{GameNode, MoveNode};
use std::collections::HashSet;

#[derive(Clone, Debug, Default)]
pub struct Goban {
    /// The size of the board. Usually this is symetrical, but I have actually played a 5x25 game.
    /// These are fun for novelty, but don't lend much to understanding the game.
    pub size: Size,

    /// I found that it was easiest to track groups of stones than to track individual stones on the
    /// board. So, I just keep track of all of the groups.
    pub groups: Vec<Group>,
}

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

        for row in 0..self.size.height {
            write!(f, " {: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 PartialEq for Goban {
    fn eq(&self, other: &Self) -> bool {
        if self.size != other.size {
            return false;
        }

        for group in self.groups.iter() {
            if !other.groups.contains(group) {
                return false;
            }
        }
        for group in other.groups.iter() {
            if !self.groups.contains(group) {
                return false;
            }
        }
        true
    }
}

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

    /// Generate a board state from an iterator of coordinates and the color of any stone present on
    /// the board. As we walk through the iterator, we play each stone as though it were being
    /// played in a game.
    ///
    /// This would not work at all if we wanted to set up an impossible board state, given that
    /// groups of stones get automatically removed once surrounded.
    pub fn from_coordinates(
        coordinates: impl IntoIterator<Item = (Coordinate, Color)>,
    ) -> Result<Self, BoardError> {
        coordinates
            .into_iter()
            .try_fold(Self::new(), |board, (coordinate, color)| {
                board.place_stone(coordinate, color)
            })
    }
}

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

impl Goban {
    /// place_stone is the most fundamental function of this object. This is as though a player put
    /// a stone on the board and evaluated the consequences.
    ///
    /// This function does not enforce turn order.
    ///
    /// # Examples
    ///
    /// ```
    /// use otg_core::{Color, Size, Coordinate, Goban};
    /// use cool_asserts::assert_matches;
    ///
    /// let goban = Goban::new();
    /// assert_eq!(goban.size, Size{ width: 19, height: 19 });
    /// let move_result = goban.place_stone(Coordinate{ column: 4, row: 4 }, Color::Black);
    /// assert_matches!(move_result, Goban);
    /// ```
    pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
        // Bail out immediately if there is already a stone at this location.
        if self.stone(&coordinate).is_some() {
            return Err(BoardError::InvalidPosition);
        }

        // Find all friendly groups adjacent to this stone. First, calculate the adjacent
        // coordinates. Then see if there is any group which contains that coordinate. If not, this
        // stone forms a new group of its own.
        //
        // A little subtle here is that this stone will be added to *every* adjoining friendly
        // group. This normally means only that a group gets bigger, but it could also cause two
        // groups to share a stone, which means they're now a single group.
        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()))
            // In fact, this last step actually connects the coordinates of those friendly groups
            // into a single large group.
            .fold(HashSet::new(), |acc, set| {
                acc.union(&set).cloned().collect()
            });

        // This is a little misnamed. This is a HashSet, not a full Group.
        friendly_group.insert(coordinate);

        // Remove all groups which contain the stones overlapping with this friendly group.
        self.groups
            .retain(|g| g.coordinates.is_disjoint(&friendly_group));

        // Generate a new friendly group given the coordinates.
        let friendly_group = Group {
            color,
            coordinates: friendly_group,
        };
        // Now add the group back to the board.
        self.groups.push(friendly_group.clone());

        // Now, find all groups adjacent to this one. Those are the only groups that this move is
        // going to impact. Calculate their liberties.
        let adjacent_groups = self.adjacent_groups(&friendly_group);
        for group in adjacent_groups {
            // Any group that has been reduced to 0 liberties should now be removed from the board.
            //
            // TODO: capture rules: we're not counting captured stones yet. Okay with some scoring
            // methods, but not all.
            if self.liberties(&group) == 0 {
                self.remove_group(&group);
            }
        }

        // Now, recalculate the liberties of this friendly group. If this group has been reduced to
        // zero liberties, after all captures have been accounted for, the move is an illegal
        // self-capture. Drop all of the work we've done and return an error.
        if self.liberties(&friendly_group) == 0 {
            return Err(BoardError::SelfCapture);
        }

        Ok(self)
    }

    /// Apply a list of moves to the board and return the final board. The moves will be played as
    /// though they are live moves played normally, but this function is for generating a board
    /// state from a game record. All of the moves will be played in the order given. This does not
    /// allow for the branching which is natural in a game review.
    ///
    /// # Examples
    ///
    /// ```
    /// use otg_core::{Color, Size, Coordinate, Goban};
    /// use cool_asserts::assert_matches;
    /// use sgf::{GameNode, MoveNode, Move};
    ///
    /// let goban = Goban::new();
    /// let moves = vec![
    ///     GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dd".to_owned()))),
    ///     GameNode::MoveNode(MoveNode::new(sgf::Color::White, Move::Move("pp".to_owned()))),
    ///     GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()))),
    /// ];
    /// let moves_: Vec<&GameNode> = moves.iter().collect();
    /// let goban = goban.apply_moves(moves_).expect("the test to have valid moves");
    ///
    /// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black));
    /// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White));
    /// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 3 }), Some(Color::Black));
    /// ```
    pub fn apply_moves<'a>(
        self,
        moves: impl IntoIterator<Item = &'a GameNode>,
    ) -> Result<Goban, BoardError> {
        let mut s = self;
        for m in moves.into_iter() {
            match m {
                GameNode::MoveNode(node) => s = s.apply_move_node(node)?,
                GameNode::SetupNode(_n) => unimplemented!("setup nodes aren't processed yet"),
            };
        }
        Ok(s)
    }

    fn apply_move_node(self, m: &MoveNode) -> Result<Goban, BoardError> {
        if let Some((row, column)) = m.mv.coordinate() {
            self.place_stone(Coordinate { row, column }, Color::from(&m.color))
        } else {
            Ok(self)
        }
    }

    pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
        self.groups
            .iter()
            .find(|g| g.contains(coordinate))
            .map(|g| g.color)
    }

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

    fn remove_group(&mut self, group: &Group) {
        self.groups.retain(|g| g != group);
    }

    fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
        let adjacent_spaces = self.group_halo(group).into_iter();
        let mut grps: Vec<Group> = Vec::new();

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

        grps
    }

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

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

    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()
    }

    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 {
    fn contains(&self, coordinate: &Coordinate) -> bool {
        self.coordinates.contains(coordinate)
    }
}

#[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(Goban)) {
        let board = Goban::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),
                /* */
                (Coordinate { column: 17, row: 0 }, Color::White),
                (Coordinate { column: 17, row: 1 }, Color::White),
                (Coordinate { column: 18, row: 1 }, Color::White),
            ]
            .into_iter(),
        )
        .unwrap();
        test(board);
    }

    #[test]
    fn it_gets_adjacencies_for_coordinate() {
        let board = Goban::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 = Goban::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(),
        )
        .unwrap();
        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: Goban| {
            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),
                ),
            ];

            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(|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(|board| {
            let board = board
                .place_stone(Coordinate { column: 10, row: 9 }, Color::Black)
                .unwrap();
            assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none());

            let board = board
                .place_stone(Coordinate { column: 2, row: 18 }, Color::Black)
                .unwrap();
            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());

            let board = board
                .place_stone(Coordinate { column: 5, row: 18 }, Color::White)
                .unwrap();
            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());
        });
    }

    #[test]
    fn self_capture_is_forbidden() {
        with_example_board(|board| {
            {
                let board = board.clone();
                let res = board.place_stone(Coordinate { column: 18, row: 0 }, Color::Black);
                assert_eq!(res, Err(BoardError::SelfCapture));
            }

            {
                let board = board.clone();
                let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black);
                assert_eq!(res, Err(BoardError::SelfCapture));
            }
        });
    }

    #[test]
    fn validate_group_comparisons() {
        {
            let b1 = Goban::from_coordinates(
                vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(),
            )
            .unwrap();
            let b2 = Goban::from_coordinates(
                vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(),
            )
            .unwrap();

            assert_eq!(b1, b2);
        }

        {
            let b1 = Goban::from_coordinates(
                vec![
                    (Coordinate { column: 7, row: 9 }, Color::White),
                    (Coordinate { column: 8, row: 10 }, Color::White),
                ]
                .into_iter(),
            )
            .unwrap();
            let b2 = Goban::from_coordinates(
                vec![
                    (Coordinate { column: 8, row: 10 }, Color::White),
                    (Coordinate { column: 7, row: 9 }, Color::White),
                ]
                .into_iter(),
            )
            .unwrap();

            assert_eq!(b1, b2);
        }
    }

    #[test]
    fn two_boards_can_be_compared() {
        let board = Goban::from_coordinates(
            vec![
                (Coordinate { column: 7, row: 9 }, Color::White),
                (Coordinate { column: 8, row: 8 }, Color::White),
                (Coordinate { column: 8, row: 10 }, Color::White),
                (Coordinate { column: 9, row: 9 }, Color::White),
                (Coordinate { column: 10, row: 9 }, Color::Black),
                (Coordinate { column: 9, row: 8 }, Color::Black),
                (Coordinate { column: 9, row: 10 }, Color::Black),
            ]
            .into_iter(),
        )
        .unwrap();

        let b1 = board
            .clone()
            .place_stone(Coordinate { column: 8, row: 9 }, Color::Black)
            .unwrap();

        let b2 = b1
            .clone()
            .place_stone(Coordinate { column: 9, row: 9 }, Color::White)
            .unwrap();

        assert_eq!(board, b2);
    }
}