use std::io::ErrorKind;

use rand::{Rng, RngCore};

use crate::consts::ALPHABET;
use crate::data::GameRules;

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum BoatDirection {
    Left,
    Right,
    Up,
    Down,

    UpLeft,
    UpRight,
    BottomLeft,
    BottomRight,
}

const _BOATS_DIRECTION: [BoatDirection; 2] = [BoatDirection::Right, BoatDirection::Down];

const _ALL_DIRECTIONS: [BoatDirection; 8] = [
    BoatDirection::Left,
    BoatDirection::Right,
    BoatDirection::Up,
    BoatDirection::Down,
    BoatDirection::UpLeft,
    BoatDirection::UpRight,
    BoatDirection::BottomLeft,
    BoatDirection::BottomRight,
];

impl BoatDirection {
    pub fn boat_directions() -> &'static [BoatDirection] {
        &_BOATS_DIRECTION
    }

    pub fn all_directions() -> &'static [BoatDirection] {
        &_ALL_DIRECTIONS
    }

    pub fn is_valid_boat_direction(&self) -> bool {
        matches!(
            self,
            BoatDirection::Left | BoatDirection::Right | BoatDirection::Up | BoatDirection::Down
        )
    }

    pub fn shift_coordinates(&self, coordinates: Coordinates, nth: usize) -> Coordinates {
        let shift = match self {
            BoatDirection::Left => (-1, 0),
            BoatDirection::Right => (1, 0),
            BoatDirection::Up => (0, -1),
            BoatDirection::Down => (0, 1),
            BoatDirection::UpLeft => (-1, -1),
            BoatDirection::UpRight => (1, -1),
            BoatDirection::BottomLeft => (-1, 1),
            BoatDirection::BottomRight => (1, 1),
        };

        Coordinates::new(
            coordinates.x + shift.0 * nth as i32,
            coordinates.y + shift.1 * nth as i32,
        )
    }
}

#[derive(
    serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd,
)]
pub struct Coordinates {
    y: i32,
    x: i32,
}

impl Coordinates {
    pub fn new<E>(x: E, y: E) -> Self
    where
        E: Into<i32>,
    {
        Self {
            x: x.into(),
            y: y.into(),
        }
    }

    pub fn is_valid(&self, rules: &GameRules) -> bool {
        self.x >= 0
            && self.y >= 0
            && self.x < rules.map_width as i32
            && self.y < rules.map_height as i32
    }

    pub fn add_x<E>(&self, x: E) -> Self
    where
        E: Into<i32>,
    {
        Self {
            x: self.x + x.into(),
            y: self.y,
        }
    }

    pub fn add_y<E>(&self, y: E) -> Self
    where
        E: Into<i32>,
    {
        Self {
            x: self.x,
            y: self.y + y.into(),
        }
    }

    pub fn dist_with(&self, other: &Self) -> usize {
        (self.x.abs_diff(other.x) + self.y.abs_diff(other.y)) as usize
    }

    pub fn human_print(&self) -> String {
        format!(
            "{}:{}",
            match self.y < 0 || self.y >= ALPHABET.len() as i32 {
                true => self.y.to_string(),
                false => ALPHABET.chars().nth(self.y as usize).unwrap().to_string(),
            },
            self.x
        )
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub struct BoatPosition {
    pub start: Coordinates,
    pub len: usize,
    pub direction: BoatDirection,
}

impl BoatPosition {
    /// Get all the coordinates occupied by the boat
    pub fn all_coordinates(&self) -> Vec<Coordinates> {
        let mut positions = Vec::with_capacity(self.len);
        for i in 0..self.len {
            positions.push(self.direction.shift_coordinates(self.start, i));
        }
        positions
    }

    /// Get all the coordinates around the boats
    pub fn neighbor_coordinates(&self, rules: &GameRules) -> Vec<Coordinates> {
        let boat_positions = self.all_coordinates();

        let mut neighbors = Vec::with_capacity(self.len);
        for i in 0..self.len {
            let boat_point = self.direction.shift_coordinates(self.start, i);
            for dir in BoatDirection::all_directions() {
                let curr_point = dir.shift_coordinates(boat_point, 1);
                if !neighbors.contains(&curr_point)
                    && !boat_positions.contains(&curr_point)
                    && curr_point.is_valid(rules)
                {
                    neighbors.push(curr_point);
                }
            }
        }
        neighbors
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
pub struct BoatsLayout(pub Vec<BoatPosition>);

impl BoatsLayout {
    /// Generate a new invalid (empty) boats layout
    pub fn new_invalid() -> Self {
        Self(vec![])
    }

    /// Generate random boats layout for given game rules
    pub fn gen_random_for_rules(rules: &GameRules) -> std::io::Result<Self> {
        let mut boats = Self(Vec::with_capacity(rules.boats_list().len()));
        let mut rng = rand::thread_rng();

        for boat in rules.boats_list() {
            let mut attempt = 0;
            loop {
                attempt += 1;

                let directions = BoatDirection::boat_directions();
                let position = BoatPosition {
                    start: Coordinates::new(
                        (rng.next_u32() % rules.map_width as u32) as i32,
                        (rng.next_u32() % rules.map_height as u32) as i32,
                    ),
                    len: boat,
                    direction: directions[rng.gen::<usize>() % directions.len()],
                };

                if boats.is_acceptable_boat_position(&position, rules) {
                    boats.0.push(position);
                    break;
                }

                if attempt >= rules.map_width * rules.map_height * 4 {
                    return Err(std::io::Error::new(
                        ErrorKind::Other,
                        "Un-usable game rules!",
                    ));
                }
            }
        }

        Ok(boats)
    }

    /// Generate boats layout that put boats at the beginning of the map
    pub fn layout_for_boats_at_beginning_of_map(rules: &GameRules) -> std::io::Result<Self> {
        let mut boats = Self(Vec::with_capacity(rules.boats_list().len()));
        let mut list = rules.boats_list();
        list.sort();
        list.reverse();

        for y in 0..rules.map_height {
            for x in 0..rules.map_width {
                if list.is_empty() {
                    break;
                }

                let position = BoatPosition {
                    start: Coordinates::new(x as i32, y as i32),
                    len: list[0],
                    direction: BoatDirection::Right,
                };

                if boats.is_acceptable_boat_position(&position, rules) {
                    list.remove(0);
                    boats.0.push(position);
                }
            }
        }

        if !list.is_empty() {
            return Err(std::io::Error::new(
                ErrorKind::Other,
                "Un-usable game rules!",
            ));
        }

        Ok(boats)
    }

    fn is_acceptable_boat_position(&self, pos: &BoatPosition, rules: &GameRules) -> bool {
        // Check if boat coordinates are valid
        if pos.all_coordinates().iter().any(|c| !c.is_valid(rules)) {
            return false;
        }

        // Check if the boat would cross another boat
        if pos
            .all_coordinates()
            .iter()
            .any(|c| self.find_boat_at_position(*c).is_some())
        {
            return false;
        }

        // Check if the boat touch another boat in a configuration where is it forbidden
        if !rules.boats_can_touch
            && pos
                .neighbor_coordinates(rules)
                .iter()
                .any(|c| self.find_boat_at_position(*c).is_some())
        {
            return false;
        }

        true
    }

    /// Check if this boats layout is valid or not
    pub fn errors(&self, rules: &GameRules) -> Vec<&'static str> {
        let mut errors = vec![];

        // Check the number of boats
        if self.0.len() != rules.boats_list().len() {
            errors.push("The number of boats is invalid!");
        }

        // Check the length of the boats
        let mut len = self.0.iter().map(|l| l.len).collect::<Vec<_>>();
        len.sort();
        let mut boats = rules.boats_list();
        boats.sort();

        if len != boats {
            errors.push("Boats lens mismatch!");
        }

        for boat_i in 0..self.0.len() {
            // Check boat coordinates
            let boat_i_coordinates = self.0[boat_i].all_coordinates();
            if boat_i_coordinates.iter().any(|c| !c.is_valid(rules)) {
                errors.push("A boat goes outside the game map!");
            }

            for boat_j in 0..self.0.len() {
                if boat_i == boat_j {
                    continue;
                }

                let boat_j_coords = self.0[boat_j].all_coordinates();
                if boat_i_coordinates.iter().any(|c| boat_j_coords.contains(c)) {
                    errors.push("A collision between two boats has been detected!");
                }

                if !rules.boats_can_touch
                    && self.0[boat_i]
                        .neighbor_coordinates(rules)
                        .iter()
                        .any(|c| boat_j_coords.contains(c))
                {
                    errors.push("Two boats are touching!");
                }
            }
        }

        errors
    }

    pub fn is_valid(&self, rules: &GameRules) -> bool {
        self.errors(rules).is_empty()
    }

    pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> {
        self.0.iter().find(|f| f.all_coordinates().contains(&pos))
    }

    pub fn number_of_boats(&self) -> usize {
        self.0.len()
    }
}

#[cfg(test)]
mod test {
    use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates};
    use crate::data::game_map::GameMap;
    use crate::data::{BotType, GameRules, PlayConfiguration, PrintableMap};

    #[test]
    fn dist_coordinates_eq() {
        let c = Coordinates::new(1, 1);
        assert_eq!(c.dist_with(&c), 0);
    }

    #[test]
    fn dist_neighbor_coordinates() {
        let c1 = Coordinates::new(1, 1);
        let c2 = Coordinates::new(1, 2);
        assert_eq!(c1.dist_with(&c2), 1);
    }

    #[test]
    fn dist_diagonal_coordinates() {
        let c1 = Coordinates::new(1, 1);
        let c2 = Coordinates::new(2, 2);
        assert_eq!(c1.dist_with(&c2), 2);
    }

    #[test]
    fn get_boat_coordinates() {
        let position = BoatPosition {
            start: Coordinates { x: 1, y: 1 },
            len: 3,
            direction: BoatDirection::Down,
        };
        let coordinates = position.all_coordinates();
        assert_eq!(
            coordinates,
            vec![
                Coordinates::new(1, 1),
                Coordinates::new(1, 2),
                Coordinates::new(1, 3),
            ]
        )
    }

    #[test]
    fn get_boat_neighbor_coordinates() {
        let rules = GameRules::random_players_rules();
        let position = BoatPosition {
            start: Coordinates { x: 0, y: 1 },
            len: 3,
            direction: BoatDirection::Down,
        };
        println!("{:?}", position.all_coordinates());
        let mut coordinates = position.neighbor_coordinates(&rules);
        coordinates.sort();
        assert_eq!(
            coordinates,
            vec![
                Coordinates::new(0, 0),
                Coordinates::new(1, 0),
                Coordinates::new(1, 1),
                Coordinates::new(1, 2),
                Coordinates::new(1, 3),
                Coordinates::new(0, 4),
                Coordinates::new(1, 4),
            ]
        )
    }

    #[test]
    fn generate_random_boats_layout() {
        let rules = GameRules::random_players_rules();
        let boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
        assert!(boats.errors(&rules).is_empty());
        let game = GameMap::new(rules, boats);
        game.print_map();
    }

    #[test]
    fn generate_random_boats_layout_without_touching_boats() {
        let mut rules = GameRules::random_players_rules();
        rules.boats_can_touch = false;
        let boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
        assert!(boats.errors(&rules).is_empty());
        let game = GameMap::new(rules, boats);
        game.print_map();
    }

    #[test]
    fn impossible_map() {
        let mut rules = GameRules::random_players_rules();
        rules.map_height = PlayConfiguration::default().min_map_height;
        rules.map_width = PlayConfiguration::default().min_map_width;
        let mut boats = Vec::new();
        for _i in 0..PlayConfiguration::default().max_boats_number {
            boats.push(PlayConfiguration::default().max_boat_len);
        }
        rules.set_boats_list(&boats);
        BoatsLayout::gen_random_for_rules(&rules).unwrap_err();
    }

    #[test]
    fn empty_boats_layout() {
        let rules = GameRules::random_players_rules();
        let boats = BoatsLayout(vec![]);
        assert!(!boats.errors(&rules).is_empty());
    }

    #[test]
    fn invalid_number_of_boats() {
        let rules = GameRules::random_players_rules();
        let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
        boats.0.remove(0);
        assert!(!boats.errors(&rules).is_empty());
    }

    #[test]
    fn boats_crossing() {
        let rules = GameRules::random_players_rules();
        let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
        boats.0[0].start = boats.0[1].start;
        assert!(!boats.errors(&rules).is_empty());
    }

    #[test]
    fn boats_touching_in_forbidden_configuration() {
        let rules = GameRules {
            map_width: 5,
            map_height: 5,
            boats_str: "1,1".to_string(),
            boats_can_touch: false,
            player_continue_on_hit: false,
            bot_type: BotType::Random,
        };

        let mut boats = BoatsLayout(vec![
            BoatPosition {
                start: Coordinates::new(0, 0),
                len: 1,
                direction: BoatDirection::Left,
            },
            BoatPosition {
                start: Coordinates::new(0, 1),
                len: 1,
                direction: BoatDirection::Left,
            },
        ]);
        boats.0[0].start = boats.0[1].start;
        assert!(!boats.errors(&rules).is_empty());
    }

    #[test]
    fn boats_outside_map() {
        let rules = GameRules::random_players_rules();
        let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
        boats.0[0].start = Coordinates::new(-1, -1);
        assert!(!boats.errors(&rules).is_empty());
    }
}