use crate::ship::items::ClientItemId;
use std::collections::{HashMap, BTreeMap};
use thiserror::Error;
use crate::entity::gateway::EntityGateway;
use crate::entity::character::{CharacterEntity, CharacterEntityId};
use crate::entity::item::{ItemDetail, ItemLocation, BankName};
use crate::entity::item::{Meseta, NewItemEntity, ItemEntity};
use crate::entity::item::tool::{Tool, ToolType};
use crate::entity::item::unit;
use crate::ship::map::MapArea;
use crate::ship::ship::ItemDropLocation;
use crate::ship::drops::{ItemDrop, ItemDropType};
use crate::ship::location::{AreaClient, RoomId};
use crate::ship::shops::ShopItem;

use crate::ship::items::bank::*;
use crate::ship::items::floor::*;
use crate::ship::items::inventory::*;
use crate::ship::items::use_tool;


pub enum TriggerCreateItem {
    Yes,
    No
}

#[derive(Error, Debug)]
#[error("")]
pub enum ItemManagerError {
    EntityGatewayError,
    NoSuchItemId(ClientItemId),
    NoCharacter(CharacterEntityId),
    CouldNotAddToInventory(ClientItemId),
    //ItemBelongsToOtherPlayer,
    Idunnoman,
    CouldNotSplitItem(ClientItemId),
    CouldNotDropMeseta,
    InvalidBankName(BankName),
    NotEnoughTools(Tool, usize, usize), // have, expected
    InventoryItemConsumeError(#[from] InventoryItemConsumeError),
    BankFull,
    WrongItemType(ClientItemId),
    UseItemError(#[from] use_tool::UseItemError),
    CouldNotBuyItem,
    CouldNotAddBoughtItemToInventory,
    ItemIdNotInInventory(ClientItemId),
    CannotGetMutItem,
    CannotGetIndividualItem,
    InvalidSlot(u8, u8), // slots available, slot attempted
    NoArmorEquipped,
    GatewayError(#[from] crate::entity::gateway::GatewayError)
}


async fn update_inventory_slots<EG: EntityGateway>(entity_gateway: &mut EG, character: &CharacterEntity, inventory: &CharacterInventory) -> Result<(), ItemManagerError> {
    for (slot, item) in inventory.iter().enumerate() {
        match item {
            InventoryItem::Individual(individual_inventory_item) => {
                entity_gateway.change_item_location(
                    &individual_inventory_item.entity_id,
                    ItemLocation::Inventory {
                        character_id: character.id,
                        slot: slot,
                        equipped: individual_inventory_item.equipped,
                    }
                ).await?
            },
            InventoryItem::Stacked(stacked_inventory_item) => {
                for entity_id in stacked_inventory_item.entity_ids.iter() {
                    entity_gateway.change_item_location(
                        entity_id,
                        ItemLocation::Inventory {
                            character_id: character.id,
                            slot: slot,
                            equipped: false,
                        }).await?
                }
            }
        }
    }
    Ok(())
}


pub struct ItemManager {
    id_counter: u32,

    character_inventory: HashMap<CharacterEntityId, CharacterInventory>,
    //character_bank: HashMap<CharacterEntityId, BTreeMap<BankName, CharacterBank>>,
    character_bank: HashMap<CharacterEntityId, CharacterBank>,
    character_floor: HashMap<CharacterEntityId, RoomFloorItems>,

    character_room: HashMap<CharacterEntityId, RoomId>,
    room_floor: HashMap<RoomId, RoomFloorItems>,
    room_item_id_counter: HashMap<RoomId, Box<dyn FnMut() -> ClientItemId + Send>>,
}

impl ItemManager {
    pub fn new() -> ItemManager {
        ItemManager {
            id_counter: 0,
            character_inventory: HashMap::new(),
            character_bank: HashMap::new(),
            character_floor: HashMap::new(),
            character_room: HashMap::new(),
            room_floor: HashMap::new(),
            room_item_id_counter: HashMap::new(),
        }
    }

    pub fn next_global_item_id(&mut self) -> ClientItemId {
        self.id_counter += 1;
        ClientItemId(self.id_counter)
    }

    // TODO: Result
    pub async fn load_character<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) -> Result<(), ItemManagerError> {
        let items = entity_gateway.get_items_by_character(&character.id).await?;
        let inventory_items = items.clone().into_iter()
            .filter_map(|item| {
                match item.location {
                    ItemLocation::Inventory{slot, equipped, ..} => Some((item.id, item.item, slot, equipped)),
                    _ => None,
                }
            })
            .fold(BTreeMap::new(), |mut acc, (id, item, slot, equipped)| {
                if item.is_stackable() {
                    if let ItemDetail::Tool(tool) = item {
                        let inventory_item = acc.entry(slot).or_insert(InventoryItem::Stacked(StackedInventoryItem {
                            entity_ids: Vec::new(),
                            item_id: self.next_global_item_id(),
                            tool: tool,
                        }));
                        if let InventoryItem::Stacked(ref mut stacked_inventory_item) = inventory_item {
                            stacked_inventory_item.entity_ids.push(id);
                        }
                    }
                }
                else {
                    acc.insert(slot, InventoryItem::Individual(IndividualInventoryItem {
                        entity_id: id,
                        item_id: self.next_global_item_id(),
                        item: item,
                        equipped: equipped,
                    }));
                }

                acc
            });

        // TODO: not using BankName anymore, clean this up
        let mut bank_items = items.into_iter()
            .filter_map(|item| {
                match item.location {
                    ItemLocation::Bank{name, ..} => Some((item.id, item.item, name)),
                    _ => None,
                }
            })
            .fold(BTreeMap::new(), |mut acc, (id, item, name)| {
                acc.entry(name).or_insert(Vec::new()).push((id, item));
                acc
            })
            .into_iter()
            .map(|(bank_name, bank_items)| {
                let stacked_bank_items = bank_items.into_iter()
                    .fold(Vec::new(), |mut acc, (id, bank_item)| {
                        if bank_item.is_stackable() {
                            let existing_item = acc.iter_mut()
                                .find(|item| {
                                    if let (BankItem::Stacked(stacked_bank_item), &ItemDetail::Tool(ref tool)) = (item, &bank_item) {
                                        stacked_bank_item.tool == *tool
                                    }
                                    else {
                                        false
                                    }
                                });
                            match existing_item {
                                Some(item) => {
                                    if let BankItem::Stacked(ref mut stacked_bank_item) = item {
                                        stacked_bank_item.entity_ids.push(id);
                                    }
                                }
                                None => {
                                    if let ItemDetail::Tool(tool) = bank_item {
                                        acc.push(BankItem::Stacked(StackedBankItem {
                                            entity_ids: vec![id],
                                            item_id: self.next_global_item_id(),
                                            tool: tool,
                                        }));
                                    }
                                },
                            }
                        }
                        else {
                            acc.push(BankItem::Individual(IndividualBankItem {
                                entity_id: id,
                                item_id: self.next_global_item_id(),
                                item: bank_item,
                            }));
                        }

                        acc
                    });
                (bank_name, CharacterBank::new(stacked_bank_items))
            })
            .collect::<BTreeMap<_, _>>();
        let inventory = CharacterInventory::new(inventory_items.into_iter().map(|(_k, v)| v).take(30).collect());
        self.character_inventory.insert(character.id, inventory);
        self.character_bank.insert(character.id, bank_items.remove(&BankName("".to_string())).unwrap_or(CharacterBank::new(Vec::new())));
        Ok(())
    }

    pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) {
        let base_inventory_id = ((area_client.local_client.id() as u32) << 21) | 0x10000;
        let inventory = self.character_inventory.get_mut(&character.id).unwrap();
        inventory.initialize_item_ids(base_inventory_id);
        let base_bank_id = ((area_client.local_client.id() as u32) << 21) | 0x20000;
        let default_bank = self.character_bank.get_mut(&character.id);//.unwrap().get_mut(&BankName("".to_string()));
        match default_bank {
            Some(default_bank) => {
                default_bank.initialize_item_ids(base_bank_id);
            },
            None => {},
        }
        self.character_room.insert(character.id, room_id);
        self.character_floor.insert(character.id, RoomFloorItems::new());
        self.room_floor.entry(room_id).or_insert(RoomFloorItems::new());

        let mut inc = 0xF0000000;
        self.room_item_id_counter.entry(room_id).or_insert(Box::new(move || {
            inc += 1;
            ClientItemId(inc)
        }));
    }

    pub fn get_character_inventory(&self, character: &CharacterEntity) -> Result<&CharacterInventory, ItemManagerError> {
        Ok(self.character_inventory.get(&character.id)
           .ok_or(ItemManagerError::NoCharacter(character.id))?)
    }

    pub fn get_character_bank(&self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> {
        Ok(self.character_bank
           .get(&character.id)
           .ok_or(ItemManagerError::NoCharacter(character.id))?)
           //.get(&BankName("".to_string()))
           //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?)
    }

    /*pub fn get_character_bank_mut(&mut self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> {
        Ok(self.character_bank
           .get_mut(&character.id)
           .ok_or(ItemManagerError::NoCharacter(character.id))?
           .entry(BankName("".to_string()))
           .or_insert(CharacterBank::new(Vec::new())))
           //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?)
    }*/

    pub fn remove_character_from_room(&mut self, character: &CharacterEntity) {
        self.character_inventory.remove(&character.id);
        self.character_floor.remove(&character.id);
        self.character_room.remove(&character.id)
            .as_ref()
            .map(|room| {
                if self.character_room.iter().find(|(_, r)| *r == room).is_none() {
                    self.room_floor.remove(room);
                }
            });
    }

    pub fn get_floor_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<&FloorItem, ItemManagerError> {
        let local_floor = self.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let room = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let shared_floor = self.room_floor.get(room).ok_or(ItemManagerError::NoCharacter(character.id))?;

        local_floor.get_item_by_id(item_id)
            .or_else(|| {
                shared_floor.get_item_by_id(item_id)
            })
            .ok_or(ItemManagerError::NoSuchItemId(item_id))
    }

    pub async fn character_picks_up_item<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, item_id: ClientItemId)
                                                            -> Result<TriggerCreateItem, ItemManagerError> {
        let local_floor = self.character_floor.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?;

        let floor_item = local_floor.get_item_handle_by_id(item_id)
            .or_else(|| {
                shared_floor.get_item_handle_by_id(item_id)
            })
            .ok_or(ItemManagerError::NoSuchItemId(item_id))?;

        let trigger_create_item = match floor_item.item() {
            Some(FloorItem::Individual(individual_floor_item)) => {
                let new_inventory_item = inventory.pick_up_individual_floor_item(&individual_floor_item);
                match new_inventory_item {
                    Some((new_inventory_item, slot)) => {
                        entity_gateway.change_item_location(
                            &new_inventory_item.entity_id,
                            ItemLocation::Inventory {
                                character_id: character.id,
                                slot: slot.0,
                                equipped: false,
                            }
                        ).await?;
                        if let Some(_) = new_inventory_item.mag() {
                            entity_gateway.change_mag_owner(&new_inventory_item.entity_id, character).await?;
                        }
                    },
                    None => {
                        return Err(ItemManagerError::CouldNotAddToInventory(item_id));
                    },
                }
                TriggerCreateItem::Yes
            },
            Some(FloorItem::Stacked(stacked_floor_item)) => {
                let new_inventory_item = inventory.pick_up_stacked_floor_item(&stacked_floor_item);

                match new_inventory_item {
                    Some((new_inventory_item, slot)) => {
                        for entity_id in &new_inventory_item.entity_ids {
                            entity_gateway.change_item_location(
                                &entity_id,
                                ItemLocation::Inventory {
                                    character_id: character.id,
                                    slot: slot.0,
                                    equipped: false,
                                }
                            ).await?;
                        }

                        if stacked_floor_item.count() != new_inventory_item.count() {
                            TriggerCreateItem::No
                        }
                        else {
                            TriggerCreateItem::Yes
                        }
                    },
                    None => {
                        return Err(ItemManagerError::CouldNotAddToInventory(item_id));
                    }
                }
            },
            Some(FloorItem::Meseta(meseta_floor_item)) => {
                if character.meseta >= 999999 {
                    return Err(ItemManagerError::CouldNotAddToInventory(item_id));
                }
                character.meseta = std::cmp::min(character.meseta + meseta_floor_item.meseta.0, 999999);
                entity_gateway.save_character(&character).await?;
                TriggerCreateItem::No
            },
            None => {
                return Err(ItemManagerError::CouldNotAddToInventory(item_id));
            }
        };

        floor_item.remove_from_floor();
        Ok(trigger_create_item)
    }

    pub async fn enemy_drop_item_on_local_floor<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, ItemManagerError> {
        let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;

        enum ItemOrMeseta {
            Individual(ItemDetail),
            Stacked(Tool),
            Meseta(Meseta)
        }

        let item = match item_drop.item {
            ItemDropType::Weapon(w) => ItemOrMeseta::Individual(ItemDetail::Weapon(w)),
            ItemDropType::Armor(w) => ItemOrMeseta::Individual(ItemDetail::Armor(w)),
            ItemDropType::Shield(w) => ItemOrMeseta::Individual(ItemDetail::Shield(w)),
            ItemDropType::Unit(w) => ItemOrMeseta::Individual(ItemDetail::Unit(w)),
            ItemDropType::TechniqueDisk(w) => ItemOrMeseta::Individual(ItemDetail::TechniqueDisk(w)),
            ItemDropType::Mag(w) => ItemOrMeseta::Individual(ItemDetail::Mag(w)),
            //ItemDropType::IndividualTool(t) => ItemOrMeseta::Individual(ItemDetail::Tool(t)),
            //ItemDropType::StackedTool(t, _) => ItemOrMeseta::Stacked(t),
            ItemDropType::Tool(t) if t.tool.is_stackable() => ItemOrMeseta::Stacked(t),
            ItemDropType::Tool(t) if !t.tool.is_stackable() => ItemOrMeseta::Individual(ItemDetail::Tool(t)),
            ItemDropType::Meseta(m) => ItemOrMeseta::Meseta(Meseta(m)),
            _ => unreachable!() // rust isnt smart enough to see that the conditional on tool catches everything
        };

        let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?();
        let floor_item = match item {
            ItemOrMeseta::Individual(item_detail) => {
                let entity = entity_gateway.create_item(NewItemEntity {
                    item: item_detail.clone(),
                    location: ItemLocation::LocalFloor {
                        character_id: character.id,
                        map_area: item_drop.map_area,
                        x: item_drop.x,
                        y: item_drop.y,
                        z: item_drop.z,
                    }
                }).await?;
                FloorItem::Individual(IndividualFloorItem {
                    entity_id: entity.id,
                    item_id: item_id,
                    item: item_detail,
                    map_area: item_drop.map_area,
                    x: item_drop.x,
                    y: item_drop.y,
                    z: item_drop.z,
                })
            },
            ItemOrMeseta::Stacked(tool) => {
                let entity = entity_gateway.create_item(NewItemEntity {
                    item: ItemDetail::Tool(tool),
                    location: ItemLocation::LocalFloor {
                        character_id: character.id,
                        map_area: item_drop.map_area,
                        x: item_drop.x,
                        y: item_drop.y,
                        z: item_drop.z,
                    }
                }).await?;
                FloorItem::Stacked(StackedFloorItem {
                    entity_ids: vec![entity.id],
                    item_id: item_id,
                    tool: tool,
                    map_area: item_drop.map_area,
                    x: item_drop.x,
                    y: item_drop.y,
                    z: item_drop.z,
                })
            },
            ItemOrMeseta::Meseta(meseta) => {
                FloorItem::Meseta(MesetaFloorItem {
                    item_id: item_id,
                    meseta: meseta,
                    map_area: item_drop.map_area,
                    x: item_drop.x,
                    y: item_drop.y,
                    z: item_drop.z,
                })
            },
        };

        self.character_floor.entry(character.id).or_insert(RoomFloorItems::new()).add_item(floor_item);
        // TODO: make these real errors
        self.character_floor.get(&character.id).ok_or(ItemManagerError::Idunnoman)?.get_item_by_id(item_id).ok_or(ItemManagerError::Idunnoman)
    }

    pub async fn player_drop_item_on_shared_floor<EG: EntityGateway>(&mut self,
                                                                     entity_gateway: &mut EG,
                                                                     character: &CharacterEntity,
                                                                     //inventory_item: InventoryItem,
                                                                     item_id: ClientItemId,
                                                                     item_drop_location: (MapArea, f32, f32, f32))
                                                                     -> Result<(), ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?;

        let dropped_inventory_item = inventory.take_item_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;

        match dropped_inventory_item {
            InventoryItem::Individual(individual_inventory_item) => {
                let individual_floor_item = shared_floor.drop_individual_inventory_item(individual_inventory_item, item_drop_location);
                entity_gateway.change_item_location(
                    &individual_floor_item.entity_id,
                    ItemLocation::SharedFloor {
                        map_area: item_drop_location.0,
                        x: item_drop_location.1,
                        y: item_drop_location.2,
                        z: item_drop_location.3,
                    }
                ).await?;
            },
            InventoryItem::Stacked(stacked_inventory_item) => {
                let stacked_floor_item = shared_floor.drop_stacked_inventory_item(stacked_inventory_item, item_drop_location);
                for entity_id in &stacked_floor_item.entity_ids {
                    entity_gateway.change_item_location(
                        entity_id,
                        ItemLocation::SharedFloor {
                            map_area: item_drop_location.0,
                            x: item_drop_location.1,
                            y: item_drop_location.2,
                            z: item_drop_location.3,
                        }
                    ).await?;
                }
            },
        }

        update_inventory_slots(entity_gateway, character, &inventory).await?;
        Ok(())
    }

    pub async fn player_drops_meseta_on_shared_floor<EG: EntityGateway>(&mut self,
                                                                        entity_gateway: &mut EG,
                                                                        character: &mut CharacterEntity,
                                                                        drop_location: ItemDropLocation,
                                                                        amount: u32)
                                                                        -> Result<FloorItem, ItemManagerError> {
        let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        if character.meseta < amount {
            return Err(ItemManagerError::CouldNotDropMeseta)
        }
        character.meseta -= amount;
        entity_gateway.save_character(&character).await?;

        let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?();
        let floor_item = FloorItem::Meseta(MesetaFloorItem {
            item_id: item_id,
            meseta: Meseta(amount),
            map_area: drop_location.map_area,
            x: drop_location.x,
            y: 0.0,
            z: drop_location.z,
        });

        shared_floor.add_item(floor_item.clone());
        Ok(floor_item)
    }

    pub async fn player_drops_partial_stack_on_shared_floor<EG: EntityGateway>(&mut self,
                                                                               entity_gateway: &mut EG,
                                                                               character: &CharacterEntity,
                                                                               //inventory_item: InventoryItem,
                                                                               item_id: ClientItemId,
                                                                               drop_location: ItemDropLocation,
                                                                               amount: usize)
                                                                               -> Result<&StackedFloorItem, ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?;

        let item_to_split = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;

        let new_item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?();
        let stacked_floor_item = shared_floor.drop_partial_stacked_inventory_item(item_to_split, amount, new_item_id, (drop_location.map_area, drop_location.x, 0.0, drop_location.z))
            .ok_or(ItemManagerError::CouldNotSplitItem(item_id))?;

        for entity_id in &stacked_floor_item.entity_ids {
            entity_gateway.change_item_location(
                entity_id,
                ItemLocation::SharedFloor {
                    map_area: drop_location.map_area,
                    x: drop_location.x,
                    y: 0.0,
                    z: drop_location.z,
                }
            ).await?;
        }

        Ok(stacked_floor_item)
    }

    pub async fn player_consumes_tool<EG: EntityGateway>(&mut self,
                                                         entity_gateway: &mut EG,
                                                         character: &CharacterEntity,
                                                         item_id: ClientItemId,
                                                         amount: usize)
                                                         -> Result<ConsumedItem, ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let used_item = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;
        let consumed_item = used_item.consume(amount)?;

        for entity_id in consumed_item.entity_ids() {
            entity_gateway.change_item_location(&entity_id,
                                                ItemLocation::Consumed).await?;
        }

        update_inventory_slots(entity_gateway, character, &inventory).await?;
        Ok(consumed_item)
    }

    pub async fn player_deposits_item<EG: EntityGateway>(&mut self,
                                                         entity_gateway: &mut EG,
                                                         character: &CharacterEntity,
                                                         item_id: ClientItemId,
                                                         amount: usize)
                                                         -> Result<(), ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let bank = self.character_bank
            .get_mut(&character.id)
            .ok_or(ItemManagerError::NoCharacter(character.id))?;

        let item_to_deposit = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;
        let bank_item = bank.deposit_item(item_to_deposit, amount).ok_or(ItemManagerError::Idunnoman)?;

        match bank_item {
            BankItem::Individual(individual_bank_item) => {
                entity_gateway.change_item_location(&individual_bank_item.entity_id,
                                                    ItemLocation::Bank {
                                                        character_id: character.id,
                                                        name: BankName("".to_string())
                                                    }).await?;
            },
            BankItem::Stacked(stacked_bank_item) => {
                for entity_id in &stacked_bank_item.entity_ids {
                    entity_gateway.change_item_location(entity_id,
                                                        ItemLocation::Bank {
                                                            character_id: character.id,
                                                            name: BankName("".to_string())
                                                        }).await?;
                }
            }
        }

        update_inventory_slots(entity_gateway, character, &inventory).await?;
        Ok(())
    }

    pub async fn player_withdraws_item<EG: EntityGateway>(&mut self,
                                                          entity_gateway: &mut EG,
                                                          character: &CharacterEntity,
                                                          item_id: ClientItemId,
                                                          amount: usize)
                                                          -> Result<&InventoryItem, ItemManagerError> {

        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let bank = self.character_bank
            .get_mut(&character.id)
            .ok_or(ItemManagerError::NoCharacter(character.id))?;

        let item_to_withdraw = bank.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;
        let inventory_item = inventory.withdraw_item(item_to_withdraw, amount).ok_or(ItemManagerError::Idunnoman)?;

        match inventory_item {
            (InventoryItem::Individual(individual_inventory_item), slot) => {
                entity_gateway.change_item_location(&individual_inventory_item.entity_id,
                                                    ItemLocation::Inventory {
                                                        character_id: character.id,
                                                        slot: slot,
                                                        equipped: false,
                                                    }).await?;
            },
            (InventoryItem::Stacked(stacked_inventory_item), slot) => {
                for entity_id in &stacked_inventory_item.entity_ids {
                    entity_gateway.change_item_location(entity_id,
                                                        ItemLocation::Inventory {
                                                            character_id: character.id,
                                                            slot: slot,
                                                            equipped: false,
                                                        }).await?;
                }
            }
        }

        Ok(inventory_item.0)
    }

    pub async fn player_feeds_mag_item<EG: EntityGateway>(&mut self,
                                                          entity_gateway: &mut EG,
                                                          character: &CharacterEntity,
                                                          mag_id: ClientItemId,
                                                          tool_id: ClientItemId)
                                                          -> Result<(), ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let consumed_tool = {
            let item_to_feed = inventory.get_item_handle_by_id(tool_id).ok_or(ItemManagerError::NoSuchItemId(tool_id))?;
            item_to_feed.consume(1)?
        };
        let mut mag_handle = inventory.get_item_handle_by_id(mag_id).ok_or(ItemManagerError::NoSuchItemId(mag_id))?;

        let individual_item = mag_handle.item_mut()
            .ok_or(ItemManagerError::NoSuchItemId(mag_id))?
            .individual()
            .ok_or(ItemManagerError::WrongItemType(mag_id))?;
        let mag = individual_item
            .mag_mut()
            .ok_or(ItemManagerError::WrongItemType(mag_id))?;

        let consumed_tool_type = match &consumed_tool {
            ConsumedItem::Stacked(stacked_consumed_item) => stacked_consumed_item.tool.tool,
            _ => return Err(ItemManagerError::WrongItemType(tool_id))
        };
        mag.feed(consumed_tool_type);

        for entity_id in consumed_tool.entity_ids() {
            entity_gateway.feed_mag(&individual_item.entity_id, &entity_id).await?;
            entity_gateway.change_item_location(&entity_id, ItemLocation::FedToMag {
                mag: individual_item.entity_id,
            }).await?;
        }

        update_inventory_slots(entity_gateway, character, &inventory).await?;
        Ok(())
    }

    pub async fn use_item<EG: EntityGateway>(&mut self,
                                             used_item: ConsumedItem,
                                             entity_gateway: &mut EG,
                                             character: &mut CharacterEntity) -> Result<(), ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        match &used_item.item() {
            ItemDetail::Weapon(_w) => {
                // something like when items are used to combine/transform them?
                //_ => {}
            },
            ItemDetail::Tool(t) => {
                match t.tool {
                    ToolType::PowerMaterial => {
                        use_tool::power_material(entity_gateway, character).await;
                    },
                    ToolType::MindMaterial => {
                        use_tool::mind_material(entity_gateway, character).await;
                    },
                    ToolType::EvadeMaterial => {
                        use_tool::evade_material(entity_gateway, character).await;
                    },
                    ToolType::DefMaterial => {
                        use_tool::def_material(entity_gateway, character).await;
                    },
                    ToolType::LuckMaterial => {
                        use_tool::luck_material(entity_gateway, character).await;
                    },
                    ToolType::HpMaterial => {
                        use_tool::hp_material(entity_gateway, character).await;
                    },
                    ToolType::TpMaterial => {
                        use_tool::tp_material(entity_gateway, character).await;
                    },
                    ToolType::CellOfMag502 => {
                        use_tool::cell_of_mag_502(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::CellOfMag213 => {
                        use_tool::cell_of_mag_213(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::PartsOfRobochao => {
                        use_tool::parts_of_robochao(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::HeartOfOpaOpa => {
                        use_tool::heart_of_opaopa(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::HeartOfPian => {
                        use_tool::heart_of_pian(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::HeartOfChao=> {
                        use_tool::heart_of_chao(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::HeartOfAngel => {
                        use_tool::heart_of_angel(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::KitOfHamburger => {
                        use_tool::kit_of_hamburger(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::PanthersSpirit => {
                        use_tool::panthers_spirit(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::KitOfMark3 => {
                        use_tool::kit_of_mark3(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::KitOfMasterSystem=> {
                        use_tool::kit_of_master_system(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::KitOfGenesis => {
                        use_tool::kit_of_genesis(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::KitOfSegaSaturn => {
                        use_tool::kit_of_sega_saturn(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::KitOfDreamcast => {
                        use_tool::kit_of_dreamcast(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::Tablet => {
                        use_tool::tablet(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::DragonScale => {
                        use_tool::dragon_scale(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::HeavenStrikerCoat => {
                        use_tool::heaven_striker_coat(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::PioneerParts => {
                        use_tool::pioneer_parts(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::AmitiesMemo => {
                        use_tool::amities_memo(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::HeartOfMorolian => {
                        use_tool::heart_of_morolian(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::RappysBeak => {
                        use_tool::rappys_beak(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::YahoosEngine => {
                        use_tool::yahoos_engine(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::DPhotonCore => {
                        use_tool::d_photon_core(entity_gateway, &used_item, inventory).await?;
                    },
                    ToolType::LibertaKit => {
                        use_tool::liberta_kit(entity_gateway, &used_item, inventory).await?;
                    },
                    _ => {}
                }
            }
            _ => {}
        }
        update_inventory_slots(entity_gateway, character, &inventory).await?;
        Ok(())
    }

    pub async fn player_buys_item<EG: EntityGateway>(&mut self,
                                                     entity_gateway: &mut EG,
                                                     character: &CharacterEntity,
                                                     shop_item: &(dyn ShopItem + Send + Sync),
                                                     item_id: ClientItemId,
                                                     amount: usize)
                                                     -> Result<&InventoryItem, ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;

        let item_detail = shop_item.as_item();
        let inventory_item = match item_detail {
            ItemDetail::Tool(tool) => {
                if tool.is_stackable() {
                    let mut item_entities = Vec::new();
                    for _ in 0..amount {
                        item_entities.push(entity_gateway.create_item(NewItemEntity {
                            location: ItemLocation::Shop,
                            item: ItemDetail::Tool(tool),
                        }).await?);
                    }
                    let floor_item = StackedFloorItem {
                        entity_ids: item_entities.into_iter().map(|i| i.id).collect(),
                        item_id: item_id,
                        tool: tool,
                        // TODO: this is gonna choke if I ever require the item being near the player for pickup
                        map_area: MapArea::Pioneer2Ep1,
                        x: 0.0,
                        y: 0.0,
                        z: 0.0,
                    };
                    let item_id = {
                        let (picked_up_item, slot) = inventory.pick_up_stacked_floor_item(&floor_item).ok_or(ItemManagerError::CouldNotAddBoughtItemToInventory)?;
                        for entity_id in &picked_up_item.entity_ids {
                            entity_gateway.change_item_location(entity_id,
                                                                ItemLocation::Inventory {
                                                                    character_id: character.id,
                                                                    slot: slot.0,
                                                                    equipped: false,
                                                                }).await?;
                        }
                        picked_up_item.item_id
                    };
                    inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))?
                }
                else {
                    let item_entity = entity_gateway.create_item(NewItemEntity {
                        location: ItemLocation::Shop,
                        item: ItemDetail::Tool(tool),
                    }).await?;
                    let floor_item = IndividualFloorItem {
                        entity_id: item_entity.id,
                        item_id: item_id,
                        item: ItemDetail::Tool(tool),
                        // TODO: this is gonna choke if I ever require the item being near the player for pickup
                        map_area: MapArea::Pioneer2Ep1,
                        x: 0.0,
                        y: 0.0,
                        z: 0.0,
                    };
                    let item_id = {
                        let (picked_up_item, slot) = inventory.pick_up_individual_floor_item(&floor_item).ok_or(ItemManagerError::CouldNotAddBoughtItemToInventory)?;
                        entity_gateway.change_item_location(&picked_up_item.entity_id,
                                                            ItemLocation::Inventory {
                                                                character_id: character.id,
                                                                slot: slot.0,
                                                                equipped: false,
                                                            }).await?;
                        picked_up_item.item_id
                    };
                    inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))?
                }
            },
            item_detail @ _ => {
                let item_entity = entity_gateway.create_item(NewItemEntity {
                    location: ItemLocation::Shop,
                    item: item_detail.clone(),
                }).await?;
                let floor_item = IndividualFloorItem {
                    entity_id: item_entity.id,
                    item_id: item_id,
                    item: item_detail,
                    // TODO: this is gonna choke if I ever require the item being near the player for pickup
                    map_area: MapArea::Pioneer2Ep1,
                    x: 0.0,
                    y: 0.0,
                    z: 0.0,
                };
                let item_id = {
                    let (picked_up_item, slot) = inventory.pick_up_individual_floor_item(&floor_item).ok_or(ItemManagerError::CouldNotAddBoughtItemToInventory)?;
                    entity_gateway.change_item_location(&picked_up_item.entity_id,
                                                        ItemLocation::Inventory {
                                                            character_id: character.id,
                                                            slot: slot.0,
                                                            equipped: false,
                                                        }).await?;
                    picked_up_item.item_id
                };
                inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))?
            },
        };
        Ok(inventory_item)
    }

    // TODO: check if slot exists before putting units into it
    pub async fn player_equips_item<EG: EntityGateway>(&mut self,
                                                       entity_gateway: &mut EG,
                                                       character: &CharacterEntity,
                                                       item_id: ClientItemId,
                                                       equip_slot: u8)
                                                       -> Result<(), ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let mut inventory_item_handle = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;
        let slot = inventory_item_handle.get_slot();
        let inventory_item = inventory_item_handle.item_mut().ok_or(ItemManagerError::CannotGetMutItem)?.individual().ok_or(ItemManagerError::CannotGetIndividualItem)?;
        inventory_item.equipped = true;
        if let ItemDetail::Unit(u) = inventory_item.item {
            if equip_slot > 0 {
                inventory_item.item = ItemDetail::Unit(unit::Unit {
                    unit: u.unit,
                    modifier: u.modifier,
                    armor_slot: ((equip_slot & 0x7) - 1) % 4, // or just be lazy and do equip_slot - 9
                });
            } else {
                inventory_item.item = ItemDetail::Unit(unit::Unit {
                    unit: u.unit,
                    modifier: u.modifier,
                    armor_slot: 0,
                });
            }
        };
        entity_gateway.change_item_location(&inventory_item.entity_id, ItemLocation::Inventory{
            character_id: character.id,
            slot: slot,
            equipped: true,
        }).await;
        entity_gateway.save_item(&ItemEntity{
            id: inventory_item.entity_id,
            location: ItemLocation::Inventory{
                character_id: character.id,
                slot: slot,
                equipped: true,
            },
            item: inventory_item.item.clone(),
        }).await;
        Ok(())
    }

    pub async fn player_unequips_item<EG: EntityGateway>(&mut self,
                                                       entity_gateway: &mut EG,
                                                       character: &CharacterEntity,
                                                       item_id: ClientItemId)
                                                       -> Result<(), ItemManagerError> {
        let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let mut inventory_item_handle = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?;
        let slot = inventory_item_handle.get_slot();
        let inventory_item = inventory_item_handle.item_mut().ok_or(ItemManagerError::CannotGetMutItem)?.individual().ok_or(ItemManagerError::CannotGetIndividualItem)?;
        inventory_item.equipped = false;
        if let ItemDetail::Unit(u) = inventory_item.item {
            inventory_item.item = ItemDetail::Unit(unit::Unit {
                unit: u.unit,
                modifier: u.modifier,
                armor_slot: 0,
            });
        };
        entity_gateway.change_item_location(&inventory_item.entity_id, ItemLocation::Inventory{
            character_id: character.id,
            slot: slot,
            equipped: false,
        }).await;
        entity_gateway.save_item(&ItemEntity{
            id: inventory_item.entity_id,
            location: ItemLocation::Inventory{
                character_id: character.id,
                slot: slot,
                equipped: false,
            },
            item: inventory_item.item.clone(),
        }).await;
        Ok(())
    }
}