use std::collections::HashMap;
use std::convert::{From, Into, TryFrom, TryInto};
use std::path::PathBuf;
use async_std::sync::{Arc, RwLock, RwLockReadGuard};
use futures::future::BoxFuture;
use futures::stream::{FuturesOrdered, Stream};

use thiserror::Error;
use rand::Rng;

use crate::ship::map::{Maps, MapBuilder};
use crate::ship::drops::DropTable;
use crate::entity::character::SectionID;
use crate::ship::monster::{load_monster_stats_table, MonsterType, MonsterStats};
use crate::ship::map::area::MapAreaLookup;
use crate::ship::map::enemy::RareMonsterAppearTable;
use crate::ship::quests;
use crate::ship::ship::{ShipError, ShipEvent};
use crate::ship::location::{MAX_ROOMS, RoomId};


#[derive(Clone)]
pub struct Rooms([Arc<RwLock<Option<RoomState>>>; MAX_ROOMS]);

impl Default for Rooms {
    fn default() -> Rooms {
        Rooms(core::array::from_fn(|_| Arc::new(RwLock::new(None))))
    }
}

impl Rooms {
    pub async fn add(&self, room_id: RoomId, room: RoomState) -> Result<(), anyhow::Error> {
        *self.0
            .get(room_id.0)
            .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
            .write()
            .await = Some(room);
        Ok(())
    }

    pub async fn remove(&self, room_id: RoomId) {
        if let Some(room) = self.0.get(room_id.0) {
            *room
                .write()
                .await = None;
        }
    }

    pub async fn exists(&self, room_id: RoomId) -> bool {
        match self.0.get(room_id.0) {
            Some(room) => {
                room
                    .read()
                    .await
                    .is_some()
            },
            None => false,
        }
    }
    
    pub async fn with<'a, T, F>(&'a self, room_id: RoomId, func: F) -> Result<T, anyhow::Error>
    where
        T: Send,
        F: for<'b> FnOnce(&'b RoomState) -> BoxFuture<'b, T> + Send + 'a
    {
        let room = self.0
            .get(room_id.0)
            .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
            .read()
            .await;
        if let Some(room) = room.as_ref() {
            Ok(func(room).await)
        }
        else {
            Err(ShipError::InvalidRoom(room_id.0 as u32).into())
        }
    }

    pub async fn with_mut<'a, T, F>(&'a self, room_id: RoomId, func: F) -> Result<T, anyhow::Error>
    where
        T: Send,
        F: for<'b> FnOnce(&'b mut RoomState) -> BoxFuture<'b, T> + Send + 'a
    {
        let mut room = self.0
            .get(room_id.0)
            .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
            .write()
            .await;

        if let Some(room) = room.as_mut() {
            Ok(func(room).await)
        }
        else {
            Err(ShipError::InvalidRoom(room_id.0 as u32).into())
        }
    }
    
    pub async fn get(&self, room_id: RoomId) -> RwLockReadGuard<Option<RoomState>> {
        self.0
            .get(room_id.0)
            .unwrap()
            .read()
            .await
    }

    pub fn stream(&self) -> impl Stream<Item = RwLockReadGuard<Option<RoomState>>> {
        self.0
            .iter()
            .map(|room| async move {
                room
                    .read()
                    .await
            })
            .collect::<FuturesOrdered<_>>()
    }
}

#[derive(Debug, Error)]
#[error("")]
pub enum RoomCreationError {
    InvalidMode,
    InvalidEpisode(u8),
    InvalidDifficulty(u8),
    CouldNotLoadMonsterStats(RoomMode),
    CouldNotLoadQuests,
}

#[derive(Debug, Copy, Clone, derive_more::Display)]
pub enum Episode {
    #[display(fmt="ep1")]
    One,
    #[display(fmt="ep2")]
    Two,
    #[display(fmt="ep4")]
    Four,
}

#[derive(Debug, Copy, Clone)]
pub enum PlayerMode{
    Single,
    Multi,
}

impl PlayerMode {
    pub fn value(&self) -> u8 {
        match self {
            PlayerMode::Single => 1,
            PlayerMode::Multi => 0,
        }
    }
}

impl TryFrom<u8> for Episode {
    type Error = RoomCreationError;

    fn try_from(value: u8) -> Result<Episode, RoomCreationError> {
        match value {
            1 => Ok(Episode::One),
            2 => Ok(Episode::Two),
            3 => Ok(Episode::Four),
            _ => Err(RoomCreationError::InvalidEpisode(value))
        }
    }
}

impl From<Episode> for u8 {
    fn from(other: Episode) -> u8 {
        match other {
            Episode::One => 1,
            Episode::Two => 2,
            Episode::Four => 3,
        }
    }
}

impl Episode {
    pub fn from_quest(value: u8) -> Result<Episode, RoomCreationError> {
        match value {
            0 => Ok(Episode::One),
            1 => Ok(Episode::Two),
            2 => Ok(Episode::Four),
            _ => Err(RoomCreationError::InvalidEpisode(value))
        }
    }
}

#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)]
pub enum Difficulty {
    Normal,
    Hard,
    VeryHard,
    Ultimate,
}

impl TryFrom<u8> for Difficulty {
    type Error = RoomCreationError;

    fn try_from(value: u8) -> Result<Difficulty, RoomCreationError> {
        match value {
            0 => Ok(Difficulty::Normal),
            1 => Ok(Difficulty::Hard),
            2 => Ok(Difficulty::VeryHard),
            3 => Ok(Difficulty::Ultimate),
            _ => Err(RoomCreationError::InvalidDifficulty(value))
        }
    }
}

impl From<Difficulty> for u8 {
    fn from(other: Difficulty) -> u8 {
        match other {
            Difficulty::Normal => 0,
            Difficulty::Hard => 1,
            Difficulty::VeryHard => 2,
            Difficulty::Ultimate => 3,
        }
    }
}

#[derive(Debug, Copy, Clone, derive_more::Display)]
pub enum RoomMode {
    #[display(fmt="single")]
    Single {
        episode: Episode,
        difficulty: Difficulty,
    },
    #[display(fmt="multi")]
    Multi {
        episode: Episode,
        difficulty: Difficulty,
    },
    #[display(fmt="challenge")]
    Challenge {
        episode: Episode,
    },
    #[display(fmt="battle")]
    Battle {
        episode: Episode,
        difficulty: Difficulty,
    }
}


impl RoomMode {
    pub fn difficulty(&self) -> Difficulty {
        match self {
            RoomMode::Single {difficulty, ..} => *difficulty,
            RoomMode::Multi {difficulty, ..} => *difficulty,
            RoomMode::Battle {difficulty, ..} => *difficulty,
            RoomMode::Challenge {..} => Difficulty::Normal,
        }
    }

    pub fn episode(&self) -> Episode {
        match self {
            RoomMode::Single {episode, ..} => *episode,
            RoomMode::Multi {episode, ..} => *episode,
            RoomMode::Battle {episode, ..} => *episode,
            RoomMode::Challenge {episode, ..} => *episode,
        }
    }

    pub fn battle(&self) -> bool {
        match self {
            RoomMode::Battle {..} => true,
            _ => false,
        }
    }

    pub fn challenge(&self) -> bool {
        match self {
            RoomMode::Challenge {..} => true,
            _ => false,
        }
    }

    pub fn player_mode(&self) -> PlayerMode {
        match self {
            RoomMode::Single {..} => PlayerMode::Single,
            _ => PlayerMode::Multi,
        }
    }
}
pub enum QuestCategoryType {
    Standard,
    Government,
}

impl From<usize> for QuestCategoryType {
    fn from(f: usize) -> QuestCategoryType {
        match f {
            0 => QuestCategoryType::Standard,
            1 => QuestCategoryType::Government,
            _ => QuestCategoryType::Standard, // TODO: panic?
        }
    }
}

impl QuestCategoryType {
    pub fn value(&self) -> usize {
        match self {
             QuestCategoryType::Standard => 0,
             QuestCategoryType::Government => 1,
        }
    }
}

pub struct RoomState {
    pub mode: RoomMode,
    pub name: String,
    pub password: [u16; 16],
    pub maps: Maps,
    pub drop_table: Box<DropTable>,
    pub section_id: SectionID,
    pub random_seed: u32,
    pub bursting: bool,
    pub monster_stats: Box<HashMap<MonsterType, MonsterStats>>,
    pub map_areas: MapAreaLookup,
    pub quest_group: QuestCategoryType,
    pub quests: Vec<quests::QuestList>,
    // items on ground
    // enemy info
}

impl RoomState {
    pub fn get_flags_for_room_list(&self) -> u8 {
        let mut flags = 0u8;
        
        match self.mode {
            RoomMode::Single {..} => {flags += 0x04}
            RoomMode::Battle {..} => {flags += 0x10},
            RoomMode::Challenge {..} => {flags += 0x20},
            _ => {flags += 0x40},
        };
        
        if self.password[0] > 0 {
            flags += 0x02;
        }
        flags
    }

    pub fn get_episode_for_room_list(&self) -> u8 {
        let episode: u8 = self.mode.episode().into();

        match self.mode {
            RoomMode::Single {..} => episode + 0x10,
            _ => episode + 0x40,
        }
    }

    pub fn get_difficulty_for_room_list(&self) -> u8 {
        let difficulty: u8 = self.mode.difficulty().into();
        difficulty + 0x22
    }

    pub fn set_quest_group(&mut self, group: usize) {
        self.quest_group = QuestCategoryType::from(group);
    }

    pub fn from_create_room(create_room: &libpso::packet::ship::CreateRoom,
                            map_builder: Arc<Box<dyn MapBuilder>>,
                            drop_table_builder: Arc<Box<dyn Fn(Episode, Difficulty, SectionID) -> DropTable + Send + Sync>>,
                            section_id: SectionID,
                            event: ShipEvent)
                            -> Result<RoomState, RoomCreationError> {
        if [create_room.battle, create_room.challenge, create_room.single_player].iter().sum::<u8>() > 1 {
            return Err(RoomCreationError::InvalidMode)
        }
        
        let room_mode = if create_room.battle == 1 {
            RoomMode::Battle {
                episode: create_room.episode.try_into()?,
                difficulty: create_room.difficulty.try_into()?,
            }
        }
        else if create_room.challenge == 1 {
            RoomMode::Challenge {
                episode: create_room.episode.try_into()?,
            }
        }
        else if create_room.single_player == 1 {
            RoomMode::Single {
                episode: create_room.episode.try_into()?,
                difficulty: create_room.difficulty.try_into()?,
            }
        }
        else { // normal multimode
            RoomMode::Multi {
                episode: create_room.episode.try_into()?,
                difficulty: create_room.difficulty.try_into()?,
            }
        };


        // push the usual set of quests for the selected mode
        let mut qpath = PathBuf::from("data/quests/bb");
        qpath.push(room_mode.episode().to_string());
        qpath.push(room_mode.to_string());
        qpath.push("quests.toml");
        let mut room_quests = Vec::new();
        let quest_list = match quests::load_quests(qpath) {
            Ok(qlist) => qlist,
            Err(_) => return Err(RoomCreationError::CouldNotLoadQuests),
        };

        room_quests.push(quest_list);

        // if multiplayer also push the government quests
        if let RoomMode::Multi {..} = room_mode {
            qpath = PathBuf::from("data/quests/bb/");
            qpath.push(room_mode.episode().to_string());
            qpath.push("government/quests.toml");

            let quest_list = match quests::load_quests(qpath) {
                Ok(qlist) => qlist,
                Err(_) => return Err(RoomCreationError::CouldNotLoadQuests),
            };

            room_quests.push(quest_list);
        }

        Ok(RoomState {
            monster_stats: Box::new(load_monster_stats_table(&room_mode).map_err(|_| RoomCreationError::CouldNotLoadMonsterStats(room_mode))?),
            mode: room_mode,
            random_seed: rand::thread_rng().gen(),
            name: String::from_utf16_lossy(&create_room.name).trim_matches(char::from(0)).into(),
            password: create_room.password,
            maps: map_builder.generate_maps(room_mode, event),
            section_id,
            drop_table: Box::new(drop_table_builder(room_mode.episode(), room_mode.difficulty(), section_id)),
            bursting: false,
            map_areas: MapAreaLookup::new(&room_mode.episode()),
            quest_group: QuestCategoryType::Standard,
            quests: room_quests,
        })
    }
}