use std::collections::{BTreeMap, BTreeSet, VecDeque};

use iter_tools::Itertools;
use rand::{seq::IteratorRandom, Rng};

use crate::{Coord, Direction};

#[allow(clippy::cast_possible_truncation)]
const MAX_HEALTH: u8 = crate::MAX_HEALTH as u8;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct SnakeToken {
    id: u8,
}

impl SnakeToken {
    pub fn from_board(board: &crate::Board) -> BTreeMap<String, Self> {
        board
            .snakes
            .iter()
            .enumerate()
            .map(|(i, snake)| {
                (
                    snake.id.clone(),
                    Self {
                        id: u8::try_from(i).expect("Way to many snakes for a single game"),
                    },
                )
            })
            .collect()
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Board {
    turn: i32,
    /// Height of the board
    height: i32,
    /// Width of the board
    width: i32,
    /// Food on the board
    food: BTreeSet<Coord>,
    /// Chance of new food spawning each round
    food_chance: u8,
    /// minimum quantity of food that must be on the board
    min_food: u8,
    /// Alive snakes
    snakes: BTreeMap<SnakeToken, Battlesnake>,
    /// True when playing constrictor mode. In this mode the snakes don't loose health and grow
    /// every turn
    constrictor: bool,
}

impl Board {
    pub fn from_game_board(
        board: &crate::Board,
        token_map: &BTreeMap<String, SnakeToken>,
        turn: i32,
        food_chance: u8,
        min_food: u8,
        constrictor: bool,
    ) -> Self {
        let width = board.width;
        debug_assert!(width > 0);
        let height = board.height;
        debug_assert!(height > 0);
        let food = board.food.iter().copied().collect();
        let snakes = board
            .snakes
            .iter()
            .map(|snake| {
                let token = token_map[&snake.id];
                (token, Battlesnake::from_game_snake(snake))
            })
            .collect();

        Self {
            turn,
            height,
            width,
            food,
            food_chance,
            min_food,
            snakes,
            constrictor,
        }
    }

    #[allow(clippy::cast_sign_loss)]
    pub const fn spaces(&self) -> usize {
        self.height as usize * self.width as usize
    }

    pub fn alive_snakes(&self) -> usize {
        self.snakes.len()
    }

    pub fn snakes(&self) -> impl Iterator<Item = SnakeToken> + '_ {
        self.snakes.keys().copied()
    }

    pub fn snake_length(&self, token: SnakeToken) -> Option<usize> {
        self.snakes.get(&token).map(|snake| snake.body.len())
    }

    pub fn simulate_actions(&mut self, actions: &BTreeMap<SnakeToken, Direction>) {
        // move snakes
        for (token, snake) in &mut self.snakes {
            snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up));
        }

        // feed snakes
        for snake in &mut self.snakes.values_mut() {
            let head = snake.head();
            if self.constrictor || self.food.remove(head) {
                snake.health = MAX_HEALTH;
            }
        }

        // kill snakes
        let alive_ids = self
            .snakes
            .iter()
            .filter(|(_, snake)| {
                // snake must have enough health
                snake.health != 0
            })
            .map(|(token, snake)| (*token, snake.body.len(), *snake.head()))
            .filter(|(_, _, head)| {
                // head in bounds
                (0..self.width).contains(&head.x) && (0..self.height).contains(&head.y)
            })
            .filter(|(_, _, head)| {
                // body collision
                !self
                    .snakes
                    .values()
                    .flat_map(|snake2| snake2.body.iter().skip(1))
                    .any(|body| body == head)
            })
            .filter(|(token, len, head)| {
                // head to head collision
                !self
                    .snakes
                    .iter()
                    .filter(|(token2, snake2)| *token2 != token && snake2.body.len() >= *len)
                    .any(|(_, snake2)| snake2.head() == head)
            })
            .map(|(token, _, _)| token)
            .collect::<Vec<_>>();
        self.snakes.retain(|token, _| alive_ids.contains(token));

        // spawn new food
        if self.food.len() < usize::from(self.min_food)
            || rand::thread_rng().gen_ratio(u32::from(self.food_chance), 100)
        {
            let free_fields = (0..self.width)
                .flat_map(|x| (0..self.height).map(move |y| Coord { x, y }))
                .filter(|coord| {
                    !self
                        .snakes
                        .values()
                        .flat_map(|snake| snake.body.iter())
                        .any(|body| body == coord)
                })
                .filter(|coord| self.food.contains(coord));
            if let Some(field) = free_fields.choose(&mut rand::thread_rng()) {
                self.food.insert(field);
            }
        }

        self.turn += 1;
    }

    pub fn simulate_until(&mut self, mut exit: impl FnMut(&Self) -> bool) {
        while !exit(self) {
            let actions = self
                .possible_actions()
                .iter()
                .map(|(token, actions)| {
                    (
                        *token,
                        actions
                            .iter()
                            .choose(&mut rand::thread_rng())
                            .copied()
                            .unwrap_or(Direction::Up),
                    )
                })
                .collect();
            self.simulate_actions(&actions);
        }
    }

    pub fn possible_actions(&self) -> BTreeMap<SnakeToken, BTreeSet<Direction>> {
        let mut actions: BTreeMap<_, BTreeSet<_>> = self
            .snakes
            .keys()
            .map(|&token| (token, enum_iterator::all::<Direction>().collect()))
            .collect();

        for (token, actions) in &mut actions {
            let snake = &self.snakes[token];
            let head = snake.head();

            actions.retain(|direction| {
                let target = head.move_to(*direction);

                // don't move out of bounds
                if !((0..self.width).contains(&target.x) && (0..self.height).contains(&target.y)) {
                    return false;
                }

                // don't collide with other snakes
                !self
                    .snakes
                    .values()
                    .flat_map(|snake| {
                        let has_eaten = snake.health == MAX_HEALTH;
                        snake
                            .body
                            .iter()
                            .take(snake.body.len() - usize::from(!has_eaten))
                    })
                    .any(|coord| *coord == target)
            });
        }

        // don't move into bigger snakes heads with only one movement option
        let bigger_snakes = self
            .snakes
            .iter()
            .sorted_unstable_by(|(_, snake1), (_, snake2)| snake2.health.cmp(&snake1.health))
            .map(|(token, snake)| {
                if actions[token].len() == 1 {
                    (
                        snake.body.len(),
                        Some(snake.head().move_to(*actions[token].first().unwrap())),
                    )
                } else {
                    (snake.body.len(), None)
                }
            })
            .collect::<Vec<_>>();
        for (token, actions) in &mut actions {
            let snake = &self.snakes[token];
            let head = snake.head();

            actions.retain(|direction| {
                let target = head.move_to(*direction);
                !bigger_snakes
                    .iter()
                    .take_while(|(length, _)| *length > snake.body.len())
                    .any(|(_, coord)| coord.map_or(false, |coord| coord == target))
            });
        }

        actions
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Battlesnake {
    /// health points
    health: u8,
    /// Body of the snake. The head is the first element in the queue
    body: VecDeque<Coord>,
}

impl Battlesnake {
    pub fn from_game_snake(snake: &crate::Battlesnake) -> Self {
        let body: VecDeque<_> = snake.body.iter().copied().collect();
        debug_assert_eq!(body.len(), usize::try_from(snake.length).unwrap());
        debug_assert!(snake.health <= crate::MAX_HEALTH);
        let health = u8::try_from(snake.health).expect("max health is 100");
        Self { health, body }
    }

    pub fn perform_action(&mut self, direction: Direction) {
        debug_assert!(!self.body.is_empty());
        // move the head along
        self.body.push_front(self.head().move_to(direction));

        // move tail
        if self.health != MAX_HEALTH {
            // only move the tail if we didn't eat
            self.body.pop_back();
        }

        // decrease helth
        self.health = self.health.saturating_sub(1);
    }

    pub fn head(&self) -> &Coord {
        debug_assert!(!self.body.is_empty());
        self.body.front().expect("not empty")
    }
}
