use std::collections::HashMap;
use async_std::sync::{Arc, RwLock, Mutex};
use futures::future::join_all;
use anyhow::Context;

use crate::entity::gateway::{EntityGateway, GatewayError};
use crate::entity::character::{CharacterEntity, CharacterEntityId};
use crate::entity::item::{ItemEntityId, ItemDetail, ItemEntity, InventoryItemEntity, BankItemEntity, BankIdentifier};
use crate::entity::item::tool::Tool;
use crate::entity::item::weapon::Weapon;
use crate::entity::item::mag::Mag;
use crate::ship::drops::ItemDrop;
use crate::ship::items::ClientItemId;
use crate::ship::items::inventory::{Inventory, InventoryItem, InventoryItemDetail, InventoryError, InventoryState};
use crate::ship::items::floor::{FloorState, FloorItem, LocalFloor, SharedFloor, FloorType};
use crate::ship::items::bank::{Bank, BankState, BankItem, BankItemDetail, BankError};
use crate::ship::location::{AreaClient, RoomId};

#[derive(thiserror::Error, Debug)]
pub enum ItemStateError {
    #[error("character {0} not found")]
    NoCharacter(CharacterEntityId),
    #[error("room {0} not found")]
    NoRoom(RoomId),
    #[error("inventory item {0} not found")]
    NoInventoryItem(ClientItemId),
    #[error("floor item {0} not found")]
    NoFloorItem(ClientItemId),
    #[error("expected {0} to be a tool")]
    NotATool(ClientItemId),
    #[error("bank item {0} not found")]
    NoBankItem(ClientItemId),
    #[error("inventory error {0}")]
    InventoryError(#[from] InventoryError),
    #[error("bank error {0}")]
    BankError(#[from] BankError),
    #[error("invalid item id {0}")]
    InvalidItemId(ClientItemId),
    #[error("invalid drop? {0:?} (this shouldn't occur)")]
    BadItemDrop(ItemDrop),
    #[error("idk")]
    Dummy,
    #[error("gateway")]
    GatewayError(#[from] GatewayError),
    #[error("tried to remove more meseta than exists: {0}")]
    InvalidMesetaRemoval(u32),
    #[error("tried to add meseta when there is no more room")]
    FullOfMeseta,
    #[error("stacked item")]
    StackedItemError(Vec<ItemEntity>),
    #[error("apply item {0}")]
    ApplyItemError(#[from] crate::ship::items::apply_item::ApplyItemError),
    #[error("item is not a mag {0}")]
    NotAMag(ClientItemId),
    #[error("item is not mag food {0}")]
    NotMagFood(ClientItemId),
    #[error("item is not sellable")]
    ItemNotSellable,
    #[error("could not modify item")]
    InvalidModifier,
    #[error("wrong item type {0}")]
    WrongItemType(ClientItemId),
}


#[derive(Clone, Debug)]
pub struct IndividualItemDetail {
    pub entity_id: ItemEntityId,
    pub item: ItemDetail,
}

impl IndividualItemDetail {
    pub fn as_weapon(&self) -> Option<&Weapon> {
        match &self.item {
            ItemDetail::Weapon(weapon) => Some(weapon),
            _ => None
        }
    }

    pub fn as_mag(&self) -> Option<&Mag> {
        match &self.item {
            ItemDetail::Mag(mag) => Some(mag),
            _ => None
        }
    }

    pub fn as_mag_mut(&mut self) -> Option<&mut Mag> {
        match &mut self.item {
            ItemDetail::Mag(mag) => Some(mag),
            _ => None
        }
    }

    pub fn as_weapon_mut(&mut self) -> Option<&mut Weapon> {
        match &mut self.item {
            ItemDetail::Weapon(weapon) => Some(weapon),
            _ => None
        }
    }


    pub fn as_client_bytes(&self) -> [u8; 16] {
        match &self.item {
            ItemDetail::Weapon(w) => w.as_bytes(),
            ItemDetail::Armor(a) => a.as_bytes(),
            ItemDetail::Shield(s) => s.as_bytes(),
            ItemDetail::Unit(u) => u.as_bytes(),
            ItemDetail::Tool(t) => t.as_individual_bytes(),
            ItemDetail::TechniqueDisk(d) => d.as_bytes(),
            ItemDetail::Mag(m) => m.as_bytes(),
            ItemDetail::ESWeapon(e) => e.as_bytes(),
        }
    }

}

#[derive(Clone, Debug)]
pub struct StackedItemDetail {
    pub entity_ids: Vec<ItemEntityId>,
    pub tool: Tool,
}

impl StackedItemDetail {
    pub fn count(&self) -> usize {
        self.entity_ids.len()
    }
}


#[derive(Clone)]
pub enum AddItemResult {
    NewItem,
    AddToStack,
    Meseta,
}

#[derive(Clone, Debug)]
struct RoomGemItemIdCounter {
    inventory: [Arc<Mutex<u32>>; 4],
    bank: [Arc<Mutex<u32>>; 4],
}

impl Default for RoomGemItemIdCounter {
    fn default() -> RoomGemItemIdCounter {
        RoomGemItemIdCounter {
            inventory: core::array::from_fn(|gem| Arc::new(Mutex::new(((gem as u32) << 21) | 0x10000))),
            bank: core::array::from_fn(|gem| Arc::new(Mutex::new(((gem as u32) << 21) | 0x20000))),
        }
    }
}

impl RoomGemItemIdCounter {
    fn inventory(&self, area_client: &AreaClient) -> Arc<Mutex<u32>> {
        self.inventory[area_client.local_client.id() as usize].clone()
    }

    fn bank(&self, area_client: &AreaClient) -> Arc<Mutex<u32>> {
        self.bank[area_client.local_client.id() as usize].clone()
    }
}


#[derive(Clone, Debug)]
pub struct ItemState {
    character_inventory: Arc<RwLock<HashMap<CharacterEntityId, RwLock<InventoryState>>>>,
    character_bank: Arc<RwLock<HashMap<CharacterEntityId, RwLock<BankState>>>>,
    character_room: Arc<RwLock<HashMap<CharacterEntityId, RoomId>>>,
    character_floor: Arc<RwLock<HashMap<CharacterEntityId, RwLock<LocalFloor>>>>,
    room_floor: Arc<RwLock<HashMap<RoomId, RwLock<SharedFloor>>>>,
    room_gem_item_ids: Arc<RwLock<HashMap<RoomId, RoomGemItemIdCounter>>>,

    room_item_id_counter: Arc<RwLock<u32>>,
}

impl Default for ItemState {
    fn default() -> ItemState {
        ItemState {
            character_inventory: Arc::new(RwLock::new(HashMap::new())),
            character_bank: Arc::new(RwLock::new(HashMap::new())),
            character_room: Arc::new(RwLock::new(HashMap::new())),
            character_floor: Arc::new(RwLock::new(HashMap::new())),
            room_floor: Arc::new(RwLock::new(HashMap::new())),
            room_gem_item_ids: Arc::new(RwLock::new(HashMap::new())),
            room_item_id_counter: Arc::new(RwLock::new(0x00810000)),
        }
    }
}

impl ItemState {
    pub async fn get_character_inventory(&self, character: &CharacterEntity) -> Result<InventoryState, anyhow::Error> {
        Ok(self.character_inventory
           .read()
           .await
           .get(&character.id)
           .ok_or_else(|| ItemStateError::NoCharacter(character.id))?
           .read()
           .await
           .clone())
    }

    pub async fn get_character_bank(&self, character: &CharacterEntity) -> Result<BankState, anyhow::Error> {
        Ok(self.character_bank
           .read()
           .await
           .get(&character.id)
           .ok_or_else(|| ItemStateError::NoCharacter(character.id))?
           .read()
           .await
           .clone())
    }
}

impl ItemState {
    async fn new_item_id(&mut self) -> Result<ClientItemId, anyhow::Error> {
        *self.room_item_id_counter
            .write()
            .await += 1;
        Ok(ClientItemId(*self.room_item_id_counter.read().await))
    }

    pub async fn load_character_inventory<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) -> Result<(), anyhow::Error> {
        let inventory = entity_gateway.get_character_inventory(&character.id).await?;
        let equipped = entity_gateway.get_character_equips(&character.id).await?;

        let inventory_items = inventory.items.into_iter()
            .map(|item| -> Result<InventoryItem, anyhow::Error> {
                Ok(match item {
                    InventoryItemEntity::Individual(item) => {
                        InventoryItem {
                            item_id: ClientItemId(0),
                            item: InventoryItemDetail::Individual(IndividualItemDetail {
                                entity_id: item.id,
                                item: item.item,
                            }),
                        }
                    },
                    InventoryItemEntity::Stacked(items) => {
                        InventoryItem {
                            item_id: ClientItemId(0),
                            item: InventoryItemDetail::Stacked(StackedItemDetail {
                                entity_ids: items.iter().map(|i| i.id).collect(),
                                tool: items.get(0)
                                    .ok_or_else(|| ItemStateError::StackedItemError(items.clone()))?
                                    .item
                                    .clone()
                                    .as_tool()
                                    .ok_or_else(|| ItemStateError::StackedItemError(items.clone()))?
                            })
                        }
                    },
                })
            })
            .collect::<Result<Vec<_>, anyhow::Error>>()?;

        let character_meseta = entity_gateway.get_character_meseta(&character.id).await?;
        let inventory_state = InventoryState {
            character_id: character.id,
            item_id_counter: Arc::new(Mutex::new(0)),
            inventory: Inventory::new(inventory_items),
            equipped,
            meseta: character_meseta,
        };

        self.character_inventory
            .write()
            .await
            .insert(character.id, RwLock::new(inventory_state));
        Ok(())
    }

    pub async fn load_character_bank<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, bank_identifier: BankIdentifier) -> Result<(), anyhow::Error> {
        let bank = entity_gateway.get_character_bank(&character.id, &bank_identifier).await?;
        let bank_items = join_all(
            bank.items.into_iter()
                .map(|item| {
                    let mut citem_state = self.clone();
                    async move {
                        Ok(match item {
                            BankItemEntity::Individual(item) => {
                                BankItem {
                                    item_id: citem_state.new_item_id().await?,
                                    item: BankItemDetail::Individual(IndividualItemDetail {
                                        entity_id: item.id,
                                        item: item.item,
                                    })
                                }
                            },
                            BankItemEntity::Stacked(items) => {
                                BankItem {
                                    item_id: citem_state.new_item_id().await?,
                                    item:  BankItemDetail::Stacked(StackedItemDetail {
                                        entity_ids: items.iter().map(|i| i.id).collect(),
                                        tool: items.get(0)
                                            .ok_or_else(|| ItemStateError::StackedItemError(items.clone()))?
                                            .item
                                            .clone()
                                            .as_tool()
                                            .ok_or_else(|| ItemStateError::StackedItemError(items.clone()))?
                                    })
                                }
                            },
                        })
                    }})
                     .collect::<Vec<_>>())
                .await
            .into_iter()
            .collect::<Result<Vec<_>, anyhow::Error>>()?;

        let bank_meseta = entity_gateway.get_bank_meseta(&character.id, &bank_identifier).await?;
        let bank_state = BankState::new(character.id, bank_identifier, Bank::new(bank_items), bank_meseta);

        self.character_bank
            .write()
            .await
            .insert(character.id, RwLock::new(bank_state));
        Ok(())
    }

    pub async fn load_character<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) -> Result<(), anyhow::Error> {
        self.load_character_inventory(entity_gateway, character).await?;
        self.load_character_bank(entity_gateway, character, BankIdentifier::Character).await?;

        Ok(())
    }

    pub async fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) {
        let mut base_item_ids = self.room_gem_item_ids
            .write()
            .await;
        let base_item_ids = base_item_ids
            .entry(room_id)
            .or_insert_with(RoomGemItemIdCounter::default);

        self.character_inventory
            .read()
            .await
            .get(&character.id)
            .unwrap()
            .write()
            .await
            .initialize_item_ids(base_item_ids.inventory(&area_client).clone())
            .await;
        self.character_bank
            .read()
            .await
            .get(&character.id)
            .unwrap()
            .write()
            .await
            .initialize_item_ids(base_item_ids.bank(&area_client))
            .await;
        self.character_room
            .write()
            .await
            .insert(character.id, room_id);
        self.character_floor
            .write()
            .await
            .insert(character.id, RwLock::new(LocalFloor::default()));
        self.room_floor
            .write()
            .await
            .entry(room_id)
            .or_insert_with(Default::default);
    }

    pub async fn remove_character_from_room(&mut self, character: &CharacterEntity) {
        self.character_inventory
            .write()
            .await
            .remove(&character.id);
        self.character_floor
            .write()
            .await
            .remove(&character.id);

        let removed = {
            self.character_room.write().await.remove(&character.id)
        };

        if let Some(room) = removed.as_ref() {
            // TODO: this looks wrong, .all(r != room) maybe?
            if self.character_room.read().await.iter().any(|(_, r)| r == room) {
                self.room_floor
                    .write()
                    .await
                    .remove(room);
            }
        }
    }

    pub async fn get_floor_item(&self, character_id: &CharacterEntityId, item_id: &ClientItemId) -> Result<(FloorItem, FloorType), anyhow::Error> {
        let local_floors = self.character_floor
            .read()
            .await;
        let local_floor = local_floors
            .get(character_id)
            .ok_or_else(|| ItemStateError::NoCharacter(*character_id))?
            .read()
            .await;
        let rooms = self.character_room
            .read()
            .await;
        let room = rooms
            .get(character_id)
            .ok_or_else(||ItemStateError::NoCharacter(*character_id))?;
        let shared_floors = self.room_floor
            .read()
            .await;
        let shared_floor = shared_floors
            .get(room)
            .ok_or_else(||ItemStateError::NoCharacter(*character_id))?
            .read()
            .await;

        local_floor.0
            .iter()
            .find(|item| item.item_id == *item_id)
            .map(|item| (item.clone(), FloorType::Local))
            .or_else(|| {
                shared_floor.0
                    .iter()
                    .find(|item| item.item_id == *item_id)
                    .map(|item| (item.clone(), FloorType::Shared))
            })
            .ok_or_else(|| ItemStateError::NoFloorItem(*item_id))
            .with_context(|| format!("character {character_id}\nlocal floors: {local_floors:#?}\nshared floors: {shared_floors:#?}"))
    }
}


#[derive(Default, Clone)]
struct ProxiedItemState {
    character_inventory: Arc<Mutex<HashMap<CharacterEntityId, InventoryState>>>,
    character_bank: Arc<Mutex<HashMap<CharacterEntityId, BankState>>>,

    //character_room: HashMap<CharacterEntityId, RoomId>,
    character_floor: Arc<Mutex<HashMap<CharacterEntityId, LocalFloor>>>,
    room_floor: Arc<Mutex<HashMap<RoomId, SharedFloor>>>,
}

#[derive(Clone)]
pub struct ItemStateProxy {
    item_state: ItemState,
    proxied_state: ProxiedItemState,
}

impl ItemStateProxy {
    pub async fn commit(self) {
        async fn copy_back<K, V>(master: &Arc<RwLock<HashMap<K, RwLock<V>>>>,
                                 proxy: Arc<Mutex<HashMap<K, V>>>)
        where
            K: Eq + std::hash::Hash,
            V: Clone,
        {
            for (key, value) in proxy.lock().await.iter() {
                if let Some(element) = master
                    .read()
                    .await
                    .get(key) {
                        *element
                            .write()
                            .await = value.clone();
                    }
            }
        }

        copy_back(&self.item_state.character_inventory, self.proxied_state.character_inventory).await;
        copy_back(&self.item_state.character_bank, self.proxied_state.character_bank).await;
        //copy_back(self.item_state.character_room, self.proxied_state.character_room).await;
        copy_back(&self.item_state.character_floor, self.proxied_state.character_floor).await;
        copy_back(&self.item_state.room_floor, self.proxied_state.room_floor).await;
    }
}


async fn get_or_clone<K, V>(master: &Arc<RwLock<HashMap<K, RwLock<V>>>>,
                            proxy: &Arc<Mutex<HashMap<K, V>>>,
                            key: K,
                            err: fn(K) -> ItemStateError) -> Result<V, anyhow::Error>
where
    K: Eq + std::hash::Hash + Copy,
    V: Clone
{
    let existing_element = master
        .read()
        .await
        .get(&key)
        .ok_or_else(|| err(key))?
        .read()
        .await
        .clone();
    Ok(proxy
       .lock()
       .await
       .entry(key)
       .or_insert_with(|| existing_element)
       .clone())

}

impl ItemStateProxy {
    pub fn new(item_state: ItemState) -> Self {
        ItemStateProxy {
            item_state,
            proxied_state: Default::default(),
        }
    }

    pub async fn inventory(&mut self, character_id: &CharacterEntityId) -> Result<InventoryState, anyhow::Error> {
        get_or_clone(&self.item_state.character_inventory,
                     &self.proxied_state.character_inventory,
                     *character_id,
                     ItemStateError::NoCharacter).await
    }

    pub async fn set_inventory(&mut self, inventory: InventoryState) {
        self.proxied_state.character_inventory.lock().await.insert(inventory.character_id, inventory);
    }

    pub async fn bank(&mut self, character_id: &CharacterEntityId) -> Result<BankState, anyhow::Error> {
        get_or_clone(&self.item_state.character_bank,
                     &self.proxied_state.character_bank,
                     *character_id,
                     ItemStateError::NoCharacter).await
    }

    pub async fn set_bank(&mut self, bank: BankState) {
        self.proxied_state.character_bank.lock().await.insert(bank.character_id, bank);
    }

    pub async fn floor(&mut self, character_id: &CharacterEntityId) -> Result<FloorState, anyhow::Error> {
        let room_id = *self.item_state.character_room.read().await.get(character_id)
            .ok_or_else(|| anyhow::Error::from(ItemStateError::NoCharacter(*character_id)))
            .with_context(|| format!("character {character_id}\nrooms: {:#?}", self.item_state.character_room))?;
        Ok(FloorState {
            character_id: *character_id,
            local: get_or_clone(&self.item_state.character_floor, &self.proxied_state.character_floor, *character_id, ItemStateError::NoCharacter).await
                .with_context(|| format!("no local_floor state: {character_id:?} {:#?}\nproxy: {:#?}", self.item_state.character_floor, self.proxied_state.character_floor))?,
            shared: get_or_clone(&self.item_state.room_floor, &self.proxied_state.room_floor, room_id, ItemStateError::NoRoom).await
                .with_context(|| format!("no share_floor state: {character_id:?} {:#?}\nproxy: {:#?}", self.item_state.room_floor, self.proxied_state.room_floor))?,
        })
    }

    pub async fn set_floor(&mut self, floor: FloorState) {
        let room_id = *self.item_state.character_room.read().await.get(&floor.character_id)
            .ok_or_else(|| anyhow::Error::from(ItemStateError::NoCharacter(floor.character_id)))
            .with_context(|| format!("character {}\nrooms: {:#?}", floor.character_id, self.item_state.character_room)).unwrap();
        self.proxied_state.character_floor.lock().await.insert(floor.character_id, floor.local);
        self.proxied_state.room_floor.lock().await.insert(room_id, floor.shared);
    }

    pub async fn new_item_id(&mut self) -> Result<ClientItemId, anyhow::Error> {
        self.item_state.new_item_id().await
    }
}