use std::collections::HashMap;
use async_std::sync::{Arc, RwLock, RwLockReadGuard};

use futures::future::BoxFuture;

use libpso::packet::ship::*;
use libpso::packet::login::Session;

use crate::common::serverstate::ClientId;
use crate::entity::account::{UserAccountEntity, UserSettingsEntity};
use crate::entity::character::CharacterEntity;
use crate::entity::item;

use crate::ship::ship::ShipError;
use crate::ship::items;
use crate::ship::map::MapArea;
use crate::ship::shops::{WeaponShopItem, ToolShopItem, ArmorShopItem};


#[derive(Clone, Default)]
pub struct Clients(Arc<RwLock<HashMap<ClientId, RwLock<ClientState>>>>);

impl Clients {
    pub async fn add(&mut self, client_id: ClientId, client_state: ClientState) {
        self.0
            .write()
            .await
            .insert(client_id, RwLock::new(client_state));
    }

    pub async fn remove(&mut self, client_id: &ClientId) -> Option<ClientState> {
        Some(self.0
            .write()
            .await
            .remove(client_id)?
            .into_inner())
    }

    pub async fn with<'a, T, F>(&'a self, client_id: ClientId, func: F) -> Result<T, anyhow::Error>
    where
        T: Send,
        F: for<'b> FnOnce(&'b ClientState) -> BoxFuture<'b, T> + Send + 'a,
    {
        let clients = self.0
            .read()
            .await;
        let client = clients
            .get(&client_id)
            .ok_or_else(|| ShipError::ClientNotFound(client_id))?
            .read()
            .await;

        Ok(func(&client).await)
    }

    pub async fn with_many<'a, T, F, const N: usize>(&'a self, client_ids: [ClientId; N], func: F) -> Result<T, anyhow::Error>
    where
        T: Send,
        F: for<'b> FnOnce([RwLockReadGuard<'b, ClientState>; N]) -> BoxFuture<'b, T> + Send + 'a,
    {
        let clients = self.0
            .read()
            .await;
        
        let mut client_states: [std::mem::MaybeUninit<RwLockReadGuard<ClientState>>; N] = unsafe {
            std::mem::MaybeUninit::uninit().assume_init()
        };

        for (cindex, client_id) in client_ids.iter().enumerate() {
            let c = clients
                .get(client_id)
                .ok_or_else(|| ShipError::ClientNotFound(*client_id))?
                .read()
                .await;
            client_states[cindex].write(c);
        }
        
        let client_states = unsafe {
            // TODO: this should just be a normal transmute but due to compiler limitations it
            // does not yet work with const generics
            // https://github.com/rust-lang/rust/issues/61956
            std::mem::transmute_copy::<_, [RwLockReadGuard<ClientState>; N]>(&client_states)
        };

        Ok(func(client_states).await)
    }

    pub async fn with_mut<'a, T, F>(&'a self, client_id: ClientId, func: F) -> Result<T, anyhow::Error>
    where
        T: Send,
        F: for<'b> FnOnce(&'b mut ClientState) -> BoxFuture<'b, T> + Send + 'a,
    {
        let clients = self.0
            .read()
            .await;
        let mut client = clients
            .get(&client_id)
            .ok_or_else(|| ShipError::ClientNotFound(client_id))?
            .write()
            .await;

        Ok(func(&mut client).await)
    }
}


#[derive(Debug, Clone, Copy)]
pub struct ItemDropLocation {
    pub map_area: MapArea,
    pub x: f32,
    pub z: f32,
    pub item_id: items::ClientItemId,
}

pub struct LoadingQuest {
    pub header_bin: Option<QuestHeader>,
    pub header_dat: Option<QuestHeader>,
}


pub struct ClientState {
    pub user: UserAccountEntity,
    pub settings: UserSettingsEntity,
    pub character: CharacterEntity,
    _session: Session,
    //guildcard: GuildCard,
    pub block: usize,
    pub item_drop_location: Option<ItemDropLocation>,
    pub done_loading_quest: bool,
    pub area: Option<MapArea>,
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub weapon_shop: Vec<WeaponShopItem>,
    pub tool_shop: Vec<ToolShopItem>,
    pub armor_shop: Vec<ArmorShopItem>,
    pub tek: Option<(items::ClientItemId, item::weapon::TekSpecialModifier, item::weapon::TekPercentModifier, i32)>,
    pub character_playtime: chrono::Duration,
    pub log_on_time: chrono::DateTime<chrono::Utc>,
}

impl ClientState {
    pub fn new(user: UserAccountEntity, settings: UserSettingsEntity, character: CharacterEntity, session: Session) -> ClientState {
        let character_playtime = chrono::Duration::seconds(character.playtime as i64);
        ClientState {
            user,
            settings,
            character,
            _session: session,
            block: 0,
            item_drop_location: None,
            done_loading_quest: false,
            area: None,
            x: 0.0,
            y: 0.0,
            z: 0.0,
            weapon_shop: Vec::new(),
            tool_shop: Vec::new(),
            armor_shop: Vec::new(),
            tek: None,
            character_playtime,
            log_on_time: chrono::Utc::now(),
        }
    }

    pub fn update_playtime(&mut self) {
        let additional_playtime = chrono::Utc::now() - self.log_on_time;
        self.character.playtime = (self.character_playtime + additional_playtime).num_seconds() as u32;
    }
}