use crate::{
    api::PlayStoneRequest,
    board::{Board, Coordinate},
    config::DatabasePath,
    database::Database,
};
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration};
use thiserror::Error;
use typeshare::typeshare;

#[derive(Debug, PartialEq, Error)]
pub enum BoardError {
    #[error("Position is invalid")]
    InvalidPosition,
    #[error("Self-capture is forbidden")]
    SelfCapture,
    #[error("Ko")]
    Ko,
}

#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Serialize, Deserialize)]
#[typeshare]
pub enum Color {
    Black,
    White,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
pub struct Size {
    pub width: u8,
    pub height: u8,
}

impl Default for Size {
    fn default() -> Self {
        Self {
            width: 19,
            height: 19,
        }
    }
}

#[derive(Debug)]
pub struct AppState {
    pub game: Option<GameState>,
    pub database: Database,
}

impl AppState {
    pub fn new(database_path: DatabasePath) -> Self {
        Self {
            game: Some(GameState::new()),
            database: Database::open_path(database_path.to_path_buf()).unwrap(),
        }
    }

    pub fn place_stone(&mut self, req: PlayStoneRequest) {
        match self.game {
            Some(ref mut game) => {
                let _ = game.place_stone(Coordinate {
                    column: req.column,
                    row: req.row,
                });
            }
            None => {}
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
pub enum Rank {
    Kyu(u8),
    Dan(u8),
    Pro(u8),
}

impl TryFrom<&str> for Rank {
    type Error = String;

    fn try_from(_: &str) -> Result<Rank, Self::Error> {
        Ok(Rank::Kyu(15))
    }
}

impl From<Rank> for String {
    fn from(r: Rank) -> String {
        match r {
            Rank::Kyu(v) => format!("{} kyu", v),
            Rank::Dan(v) => format!("{} dan", v),
            Rank::Pro(v) => format!("{} pro", v),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Player {
    pub name: String,
    pub rank: Option<Rank>,
}

#[derive(Debug)]
pub struct GameState {
    pub board: Board,
    pub past_positions: Vec<Board>,

    pub conversation: Vec<String>,
    pub current_player: Color,

    pub white_player: Player,
    pub black_player: Player,

    pub white_clock: Duration,
    pub black_clock: Duration,
}

impl GameState {
    pub fn new() -> GameState {
        GameState {
            board: Board::new(),
            past_positions: vec![],
            conversation: vec![],
            current_player: Color::Black,
            white_player: Player {
                name: "".to_owned(),
                rank: None,
            },
            black_player: Player {
                name: "".to_owned(),
                rank: None,
            },
            white_clock: Duration::from_secs(600),
            black_clock: Duration::from_secs(600),
        }
    }

    fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
        let board = self.board.clone();
        let new_board = board.place_stone(coordinate, self.current_player)?;

        if self.past_positions.contains(&new_board) {
            return Err(BoardError::Ko);
        }

        self.past_positions.push(self.board.clone());
        self.board = new_board;
        match self.current_player {
            Color::White => self.current_player = Color::Black,
            Color::Black => self.current_player = Color::White,
        };
        Ok(())
    }
}

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

    #[test]
    fn current_player_changes_after_move() {
        let mut state = GameState::new();
        assert_eq!(state.current_player, Color::Black);
        state.place_stone(Coordinate { column: 9, row: 9 }).unwrap();
        assert_eq!(state.current_player, Color::White);
    }

    #[test]
    fn current_player_remains_the_same_after_self_capture() {
        let mut state = GameState::new();
        state.board = Board::from_coordinates(
            vec![
                (Coordinate { column: 17, row: 0 }, Color::White),
                (Coordinate { column: 17, row: 1 }, Color::White),
                (Coordinate { column: 18, row: 1 }, Color::White),
            ]
            .into_iter(),
        )
        .unwrap();
        state.current_player = Color::Black;

        assert_eq!(
            state.place_stone(Coordinate { column: 18, row: 0 }),
            Err(BoardError::SelfCapture)
        );
        assert_eq!(state.current_player, Color::Black);
    }

    #[test]
    fn ko_rules_are_enforced() {
        let mut state = GameState::new();
        state.board = Board::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();

        state.place_stone(Coordinate { column: 8, row: 9 }).unwrap();
        assert_eq!(
            state.place_stone(Coordinate { column: 9, row: 9 }),
            Err(BoardError::Ko)
        );
    }
}
