
use core::num;
use core::panic;
use numpy::Ix1;
use numpy::PyArray;
use numpy::PyArrayMethods;
use numpy::PyUntypedArrayMethods;
use numpy::ToPyArray;
use pyo3::exceptions::PyValueError;
use pyo3::ffi::PyPreConfig_InitIsolatedConfig;
use pyo3::prelude::*;
use pyo3::types::PyAnyMethods;
use pyo3::types::PyInt;
use pyo3::types::PyTuple;
use std::collections::HashSet;
use std::hash::RandomState;
use std::sync::Arc;
use pyo3_stub_gen::{create_exception, define_stub_info_gatherer, derive::*, module_variable};
use superttt::game::play_game_multiple_threads as play_game_multiple_threads_rs;
use superttt::game::player;
// use superttt::game::state;
use superttt::game::player::Player;
use superttt::game::player::RandomBot;
use superttt::game::state::HasState;
use superttt::game::state::State;

/// Functions that should be accessible in Python:
///
/// GAMES:
/// Creating players (randombot ok for now)
/// Creating games
///
use superttt::game::Game;
use superttt::game::MoveError;

#[gen_stub_pyclass]
#[pyclass(name = "State", freelist = 500)]
/// Represent a Pythonlike state. The current board, next player, and all possible moves.
/// Uses Numpy arrays when possible.
///
/// Important note:
/// * board - this attribute is a 1D-array. The first 9 elements represent the first field (upper left), the next 9 the second field (upper center) and so on
/// ```text
/// Layout:
///  0  1  2        9  10  11       18  19  20
///  3  4  5       12  13  14       21  22  23
///  6  7  8       15  16  17       24  25  26
///  
/// 27  28  29     36  37  38       45  46  47
/// 30  31  32     39  40  41       48  49  50
/// 33  34  35     42  43  44       51  52  53
///
/// 54  55  56     63  64  65       72  73  74
/// 57  58  59     66  67  68       75  76  77
/// 60  61  62     69  70  71       78  79  80
/// ```
pub struct PyState {
    #[pyo3(get)]
    board: Py<PyArray<i8, Ix1>>,
    #[pyo3(get)]
    player_id: i8,
    #[pyo3(get)]
    possible_moves: Py<PyArray<i8, Ix1>>,
}

#[gen_stub_pymethods]
#[pymethods]
impl PyState {
    // macros?
    const vb: char = '\u{002502}'; // vertical bar
    const hb: char = '\u{002500}'; // horizontal bar
    const c: char = '\u{00253C}'; // cross (corner of roster)
    const fvb: char = '\u{002503}'; // fat vertical bar
    const fhb: char = '\u{002501}'; // fat horizontal bar
    const fc: char = '\u{00254B}'; // fat cross
                                   // this might seem like a bad practice; however constants are inlined, so I believe that the performance hit will be negligble
                                   // The benefit is more readable code. 0-9 belong to the same subcell, not to the same line. This is why I opt to use this constant array.
    const idx: [usize; 81] = [
        0, 1, 2, 9, 10, 11, 18, 19, 20, 3, 4, 5, 12, 13, 14, 21, 22, 23, 6, 7, 8, 15, 16, 17, 24,
        25, 26, 27, 28, 29, 36, 37, 38, 45, 46, 47, 30, 31, 32, 39, 40, 41, 48, 49, 50, 33, 34, 35,
        42, 43, 44, 51, 52, 53, 54, 55, 56, 63, 64, 65, 72, 73, 74, 57, 58, 59, 66, 67, 68, 75, 76,
        77, 60, 61, 62, 69, 70, 71, 78, 79, 80,
    ];

    fn __str__<'py>(&self, py: Python<'py>) -> String {
        let mut out = "Board:\n".to_owned();
        unsafe {
            //* print board:
            // first line: do not add newline
            for i in 0..9 {
                // do not print in the front of the line
                if i != 0 {
                    if i % 3 != 0 {
                        // print small Self::vb
                        out += &format!("{}", Self::vb);
                    } else {
                        // divider between subfields:
                        out += &format!("{}", Self::fvb);
                    }
                }
                out += &format!(
                    " {} ",
                    &self
                        .board
                        .bind(py)
                        .get(*Self::idx.get(i).unwrap())
                        .unwrap()
                        .to_string()
                );
            }
            // add newlines
            let mut num_newlines = 1;
            for i in 9..81 {
                if i % 9 == 0 {
                    // newline
                    out += "\n";
                    //? there are 9 cells in a line, but we add 8 bars between these fields: so 17 characters
                    // * pattern: Self::hb cross Self::hb cross Self::hb
                    if num_newlines % 3 == 0 {
                        out += &format!("{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}", Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fc, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fc, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb, Self::fhb);
                    } else {
                        out += &format!(
                            "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::c,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::c,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::fvb,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::c,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::c,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::fvb,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::c,
                            Self::hb,
                            Self::hb,
                            Self::hb,
                            Self::c,
                            Self::hb,
                            Self::hb
                        );
                    }
                    out += "\n";
                    num_newlines += 1;
                } else if i % 3 != 0 {
                    // small vertical bar
                    out += &format!("{}", Self::vb);
                } else {
                    // fat vertical bar (between subfields)
                    out += &format!("{}", Self::fvb);
                }
                out += &format!(
                    " {} ",
                    &self
                        .board
                        .bind(py)
                        .get(*Self::idx.get(i).unwrap())
                        .unwrap()
                        .to_string()
                );
            }
            //* print next player:
            out += &format!("\n\nNext Player: {}\n", self.player_id);
            //* print possible moves:
            out += &format!("\nPossible Moves: {}\n", self.possible_moves);
        };
        out
    }

    /// Coerces a pystate into a checkpoint. 
    /// 
    /// This can be used to load in a checkpoint. 
    /// Method is rather expensive.
    /// 
    /// Note:
    /// The reason why checkpoints and states are not the same is a design choice. 
    /// We wanted the state to be as minimal as possible.
    fn to_checkpoint(&self) -> PyCheckpoint {
        let mut game = Game::new(0, false);
        let mut rosters = [-1_i8; 9];
        let mut board = [0_i8; 81];
        let mut next_roster = 10;
        let mut next_player = 1;
        let mut available_moves: HashSet<i8, RandomState> = HashSet::from_iter(0..81);
        Python::with_gil(|py| {
            let arr = self.board.bind(py);
            for i in 0..arr.len() {
                unsafe {
                    match arr.get(i).unwrap() {
                        1 | 2 => {
                            available_moves.remove(&(i as i8));
                            board[i] = *arr.get(i).unwrap();
                            if *arr.get(i).unwrap() == 1 {
                                next_player = 2;
                            }else{
                                next_player = 1;
                            }
                        }
                        _ => {}
                    }
                }
            }
            let moves = self.possible_moves.bind(py);
            if moves.len() > 9 {
                // automatically we know that the next roster can be anything
                next_roster = -1;
            } else {
                unsafe {
                    next_roster = moves.get(0).unwrap() / 9;
                    for idx in 1..moves.len() {
                        if next_roster != moves.get(idx).unwrap() / 9 {
                            // this means that any move is possible, but there are less than 9 cell left
                            next_roster = -1;
                            break;
                        }
                    }

                }
            }
        });
        game.set_board(board);
        for (idx, player) in board.iter().enumerate() {
            if *player != 0 {
                if game.check_three_in_a_row(*player, idx as i8) {
                    rosters[(idx as i8 / 9) as usize] = *player;
                } else {
                    let start_cell = (idx as i8 / 9) * 9;
                    let mut roster_filled = true;
                    for cell in start_cell..start_cell + 9 {
                        // if there still exist moves that can be made, just return out of the function
                        if available_moves.contains(&cell) {
                            roster_filled = false;
                            break;
                        }
                    }
                    rosters[(idx as i8 / 9) as usize] = 0;
                }
            }
        }
        game.set_empty_cells(available_moves);
        game.set_rosters(rosters);
        game.set_previous_move([next_player, next_roster]);

        PyCheckpoint(game.make_state())
    }
}

#[gen_stub_pyclass]
#[pyclass(name = "Checkpoint")]
pub struct PyCheckpoint(State);

#[gen_stub_pymethods]
#[pymethods]
/// This should only be created in other methods (after making a move)
/// Still need to think about where this should be implemented.
///
/// Note: We could give the decision where to use it to the python-programmers?
/// Perhaps use serde?
impl PyCheckpoint {
    fn get_odds(&self) -> [f64; 3] {
        self.0.get_odds()
    }
    fn __str__(&self) -> String {
        format!("{}", self.0)
    }
}

struct PyMoveError(MoveError);

impl From<MoveError> for PyMoveError {
    fn from(value: MoveError) -> Self {
        PyMoveError(value)
    }
}

impl std::convert::From<PyMoveError> for PyErr {
    fn from(error: PyMoveError) -> Self {
        PyValueError::new_err(error.0.to_string())
    }
}

#[gen_stub_pyclass]
#[pyclass(name = "Game")]
/// Game class
///
/// # Methods:
/// * get_available_moves() - Returns a vector containing the cells that are `valid` in the current state.
/// * make_move(input, player_id_authentication)
pub struct PyGame(Game);

#[gen_stub_pymethods]
#[pymethods]
impl PyGame {
    #[new]
    fn __new__(id: u32) -> PyGame {
        PyGame(Game::new(id, false))
    }

    fn get_available_moves(&self) -> Vec<i8> {
        self.0.get_available_moves()
    }

    /// Make a move. This does nothing else.
    ///
    ///
    /// ## Args
    /// * `input` - the cell that the current player makes a move in.
    /// * `player_id_authentication` - the id of the player that is making the move. This should be one of: {1, 2}
    ///
    fn make_move(&mut self, input: i8, player_id_authentication: i8) -> Result<bool, PyMoveError> {
        let not_finished = self.0.make_move(input, player_id_authentication)?;
        Ok(not_finished)
    }

    /// Returns the id of the winner of the game.
    ///
    /// If it is equal to 0, then the game ended in a draw.
    ///
    fn get_winner(&self) -> i8 {
        match self.0.get_winner() {
            Some(s) => s,
            None => 0,
        }
    }

    /// Create a checkpoint. This can be saved somewhere and loaded in at a later point in time.
    fn make_checkpoint(&self) -> PyCheckpoint {
        PyCheckpoint(self.0.make_state())
    }

    /// Load in a checkpoint from a PyCheckpoint object.
    ///
    /// ## Args
    /// * `checkpoint` - checkpoint to load in.
    fn load_checkpoint(&mut self, checkpoint: &PyCheckpoint) -> () {
        self.0.load_state(&checkpoint.0);
    }

    /// Create a `PyState`-object.
    /// This object is a simple representation of the current game.
    fn make_state<'py>(&self, py: Python<'py>) -> PyState {
        PyState {
            board: self.0.get_board().to_pyarray(py).unbind(),
            player_id: self.0.get_previous_move()[0],
            possible_moves: self.0.get_available_moves().to_pyarray(py).unbind(),
        }
    }

    /// Python __str__ method
    fn __str__(&self) -> String {
        format!("{}", self.0)
    }
}

#[gen_stub_pyclass]
#[pyclass(subclass, name = "Player")]
pub struct PyPlayer(Arc<dyn Player + Send + Sync>);

#[gen_stub_pymethods]
#[pymethods]
impl PyPlayer {
    #[new]
    fn __new__(id: i8) -> PyPlayer {
        PyPlayer(Arc::new(RandomBot::new(id)))
    }

    /// Make a move with the current player.
    /// The default implementation is naive (makes a random move).
    ///
    /// Override this method to have your custom heuristic.
    fn make_move<'py>(&self, game: &Bound<'py, PyGame>) -> () {
        // let args = PyTuple::new(py, &[game]).unwrap();
        // slf.call_method1(py, "make_move", args).unwrap();

        self.0.make_move(&mut game.borrow_mut().0);
    }

    /// Increment the number of wins for a player by one.
    fn increment_wins(&self) -> () {
        self.0.increment_wins();
    }
    /// Get the total number of wins for a player.
    fn get_wins(&self) -> u32 {
        self.0.get_wins()
    }
}

#[gen_stub_pyfunction]
#[pyfunction]
/// Play multiple games in a single-threaded environment
fn play_multiple_games<'py>(
    py: Python<'py>,
    player1: Bound<'py, PyPlayer>,
    player2: Bound<'py, PyPlayer>,
    number_of_games: Bound<'py, PyInt>,
) -> Vec<PyState> {
    let num: u32 = match number_of_games.extract() {
        Ok(o) => o,
        Err(e) => {
            panic!("{}\nCannot simulate more games than what a u32 allows. You definitely do not need to run more games than that.",e)
        }
    };
    let mut states = Vec::new();
    for _ in 0..num {
        let game = Bound::new(py, PyGame::__new__(0)).unwrap();
        states.extend(play_game(py, &player1, &player2, game));
    }
    states
}

#[gen_stub_pyfunction]
#[pyfunction]
/// Play one game between two player objects.
/// Delegates to overridden make_move method if it exists.
///
/// Todo: add caching for speeding up perhaps?
fn play_game<'py>(
    py: Python<'py>,
    player1: &Bound<'py, PyPlayer>,
    player2: &Bound<'py, PyPlayer>,
    game: Bound<'py, PyGame>,
) -> Vec<PyState> {
    let mut states: Vec<PyState> = Vec::new();
    let mut player_turn = game.try_borrow_mut().unwrap().0.get_previous_move()[0];
    while !game.try_borrow_mut().unwrap().0.is_finished() {
        if player_turn % 2 == 1 {
            // if player contains make move method:
            // check this with the getattr method
            match (
                player1.is_exact_instance_of::<PyPlayer>(),
                player1.is_instance_of::<PyPlayer>(),
            ) {
                (false, true) => {
                    // this is what we want! This means that the object is a subclass of pyplayer. i.e. it is implemented by the python user
                    let args = PyTuple::new(py, &[&game]).unwrap();
                    player1.call_method1("make_move", args).unwrap();
                }
                _ => {
                    player1.try_borrow_mut().unwrap().make_move(&game);
                }
            }
            player_turn = 2;
        } else {
            match (
                player2.is_exact_instance_of::<PyPlayer>(),
                player2.is_instance_of::<PyPlayer>(),
            ) {
                (false, true) => {
                    // this is what we want! This means that the object is a subclass of pyplayer. i.e. it is implemented by the python user
                    let args = PyTuple::new(py, &[&game]).unwrap();
                    player2.call_method1("make_move", args).unwrap();
                }
                _ => {
                    player2.try_borrow_mut().unwrap().make_move(&game);
                }
            }
            player_turn = 1;
        }
        states.push(game.borrow().make_state(py));
    }
    match game.try_borrow_mut().unwrap().0.get_winner() {
        Some(s) => {
            if s == 1 {
                player1.try_borrow_mut().unwrap().0.increment_wins();
            } else if s == 2 {
                player2.try_borrow_mut().unwrap().0.increment_wins();
            }
        }
        None => {}
    }
    return states;
}

// fn play_multiple_games_multithreaded<'py>(
//     py: Python<'py>,
//     player1: Bound<'py, PyPlayer>,
//     player2: Bound<'py, PyPlayer>,
//     number_of_games: Bound<'py, PyInt>,
//     number_of_threads: Bound<'py, PyInt>,
// ) {
//     let number_of_games = number_of_games
//         .extract::<u32>()
//         .expect("COULD NOT EXTRACT U32");
//     let number_of_threads = number_of_threads
//         .extract::<u32>()
//         .expect("COULD NOT EXTRACT U32");
//     // pyo3_async_runtimes::tokio::future_into_py(py, async {
//     //     for _ in 0..number_of_games {
//     //         let mut game = PyGame::__new__(0);
//     //         play_game(py, &player1, &player2, game);
//     //     }
//     // });

//     let rt = pyo3_async_runtimes::tokio::get_runtime();
//     let fut = async {
//         py.allow_threads(move || {
//             Python::with_gil(|py| {
//                 let mut game =
//                     Bound::new(py.clone(), PyGame::__new__(0)).expect("COULD NOT CREATE BOUND");
//                 play_game(py, &player1, &player2, game);
//             })
//         });
//         Ok(())
//     };

//     let res = pyo3_async_runtimes::tokio::future_into_py_with_locals(
//         py,
//         pyo3_async_runtimes::tokio::get_current_locals(py).expect("COULD NOT GET LOCALS"),
//         fut,
//     );
//     // rt.spawn(async{
//     //     let mut game = Bound::new(py, PyGame::__new__(0)).expect("COULD NOT CREATE BOUND");
//     //     play_game(py, &player1, &player2, game);
//     // });
// }

/// A Python module implemented in Rust.
#[pymodule(name = "superttt")]
fn superttt_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<PyGame>()?;
    m.add_class::<PyPlayer>()?;
    m.add_class::<PyCheckpoint>()?;
    m.add_class::<PyState>()?;
    m.add_function(wrap_pyfunction!(play_game, m)?)?;
    m.add_function(wrap_pyfunction!(play_multiple_games, m)?)?;
    Ok(())
}

define_stub_info_gatherer!(stub_info);