use crate::map::{Room, VertexKey, WumpusMap};
use crate::{dprint, Args};
use anyhow::Result;
use rand::rngs::ThreadRng;
use rand::seq::SliceRandom;
use slotmap::Key;

#[derive(Debug)]
struct WumpusLocation {
    wumpus: VertexKey,
    wumpus_halo: Vec<VertexKey>,
}

impl WumpusLocation {
    fn new() -> Self {
        let wumpus = VertexKey::null();
        let wumpus_halo = Vec::new();
        Self {
            wumpus,
            wumpus_halo,
        }
    }

    fn set_location(&mut self, location: VertexKey, wumpus_map: &WumpusMap) -> Result<()> {
        self.wumpus = location;
        self.wumpus_halo.clear();
        let vertex = wumpus_map.try_get_vertex(&self.wumpus)?;
        self.wumpus_halo.extend_from_slice(vertex.neighbors());
        Ok(())
    }

    fn is_near(&self, key: &VertexKey, wumpus_map: &WumpusMap) -> bool {
        if self.wumpus_halo.contains(key) {
            return true;
        }
        self.wumpus_halo
            .iter()
            .any(|v| wumpus_map.get_vertex(v).has_neighbor(key))
    }
}

#[derive(Debug)]
pub struct Things {
    pub(crate) player: VertexKey,
    wumpus_location: WumpusLocation,
    bats: Vec<VertexKey>,
    pits: Vec<VertexKey>,
    pub(crate) wumpus_movement_likelihood: u32,
}

impl Things {
    pub fn _new(conf: &Args, wumpus_map: &WumpusMap) -> Self {
        Self::try_new(conf, wumpus_map).unwrap()
    }

    pub fn try_new(conf: &Args, wumpus_map: &WumpusMap) -> Result<Self> {
        fn place_player(
            wumpus_location: &WumpusLocation,
            conf: &Args,
            vertices: &[VertexKey],
            rng: &mut ThreadRng,
            wumpus_map: &WumpusMap,
        ) -> VertexKey {
            let tunnels = conf.tunnels as f32;
            let rooms = conf.rooms as f32;
            let allow_wumpocalypse = conf.hard && (tunnels / rooms < 0.4);
            loop {
                let player = vertices.choose(rng).expect("Bug: `Vertices` is empty");
                if *player != wumpus_location.wumpus
                    && !(allow_wumpocalypse && wumpus_location.is_near(player, wumpus_map))
                {
                    return player.clone();
                }
            }
        }

        let wumpus_movement_likelihood = 2;
        let bat_count = conf.bats;
        let pit_count = conf.pits;
        let vertices = wumpus_map.as_slice();
        let mut rng = rand::thread_rng();
        let mut bats_and_pits: Vec<VertexKey> = vertices
            .choose_multiple(&mut rng, (bat_count + pit_count) as usize)
            .cloned()
            .collect();
        let bats = bats_and_pits.split_off(pit_count as usize);
        let pits = bats_and_pits;
        dprint!("<pit in rooms {:?}>\n", pits);
        dprint!("<bat in rooms {:?}>\n", bats);
        let wumpus = vertices
            .choose(&mut rng)
            .expect("Bug: `Vertices` empty")
            .clone();
        let mut wumpus_location = WumpusLocation::new();
        wumpus_location.set_location(wumpus, wumpus_map)?;
        dprint!("<wumpus in room {:?}>\n", wumpus_location);
        wumpus_location.set_location(wumpus, wumpus_map)?;
        let player = place_player(&wumpus_location, conf, vertices, &mut rng, wumpus_map);
        Ok(Self {
            wumpus_location,
            bats,
            pits,
            player,
            wumpus_movement_likelihood,
        })
    }

    pub fn wumpus_is_near(&self, wumpus_map: &WumpusMap) -> bool {
        self.wumpus_location.is_near(&self.player, wumpus_map)
    }

    pub fn bat_is_near(&self, wumpus_map: &WumpusMap) -> bool {
        let player = wumpus_map
            .try_get_vertex(&self.player)
            .expect(format!("Bug: no vertex found for player {:?}", &self.player).as_str());
        player.neighbors().iter().any(|v| self.bats.contains(v))
    }

    pub fn pit_is_near(&self, wumpus_map: &WumpusMap) -> bool {
        let player = wumpus_map
            .try_get_vertex(&self.player)
            .expect(format!("Bug: no vertex found for player {:?}", &self.player).as_str());
        player.neighbors().iter().any(|v| self.pits.contains(v))
    }

    pub fn wump_room<'a>(&self, wumpus_map: &'a WumpusMap) -> Result<&'a Room> {
        wumpus_map.try_get_room(&self.wumpus_location.wumpus)
    }

    pub fn set_wumpus_location(&mut self, key: &VertexKey, wumpus_map: &WumpusMap) -> Result<()> {
        self.wumpus_location.set_location(*key, wumpus_map)
    }

    pub fn player_wumpus_collision(&self) -> bool {
        self.player == self.wumpus_location.wumpus
    }

    pub fn get_wumpus_location(&self) -> VertexKey {
        self.wumpus_location.wumpus
    }

    pub fn set_player_location(&mut self, key: &VertexKey) {
        self.player = *key;
    }

    pub fn player_pit_collision(&self) -> bool {
        self.pits.contains(&self.player)
    }

    pub fn player_bat_collision(&self) -> bool {
        self.bats.contains(&self.player)
    }
}

pub(crate) fn initialize_things_in_cave(wumpus_map: &WumpusMap, conf: &Args) -> Result<Things> {
    let things = Things::try_new(conf, wumpus_map)?;
    Ok(things)
}