use std::collections::{HashMap, BTreeMap};
use libpso::character::character;//::InventoryItem;
use thiserror::Error;
use crate::entity::gateway::EntityGateway;
use crate::entity::character::{CharacterEntity, CharacterEntityId};
use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation};
use crate::entity::item::{Meseta, NewItemEntity};
use crate::entity::item::tool::Tool;
use crate::ship::map::MapArea;
use crate::ship::ship::ItemDropLocation;
use crate::ship::drops::{ItemDrop, ItemDropType};
use crate::ship::location::{AreaClient, RoomId};


#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ActiveItemId(pub u32);

#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
struct RoomItemId(RoomId, u32);

#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ClientItemId(pub u32);

#[derive(Debug, Clone, PartialEq)]
pub enum ActiveItemEntityId {
    Individual(ItemEntityId),
    Stacked(Vec<ItemEntityId>),
    Meseta(Meseta),
}


#[derive(Debug, Clone, PartialEq)]
pub enum HeldItemType {
    Individual(ItemDetail),
    Stacked(Tool, usize),
}

impl HeldItemType {
    pub fn as_client_bytes(&self) -> [u8; 16] {
        match self {
            HeldItemType::Individual(item) => {
                match &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(),
                }
            },
            HeldItemType::Stacked(tool, count) => {
                tool.as_stacked_bytes(*count)
            },
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum FloorItemType {
    Individual(ItemDetail),
    Stacked(Tool, usize),
    Meseta(Meseta),
}

impl FloorItemType {
    pub fn as_client_bytes(&self) -> [u8; 16] {
        match self {
            FloorItemType::Individual(item) => {
                match &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(),
                }
            },
            FloorItemType::Stacked(tool, count) => {
                tool.as_stacked_bytes(*count)
            },
            FloorItemType::Meseta(m) => {
                m.as_bytes()
            }
        }
    }
}

#[derive(Debug, Clone)]
pub struct InventoryItem {
    pub entity_id: ActiveItemEntityId,
    pub item_id: ClientItemId,
    pub item: HeldItemType,
    pub equipped: bool,
}

#[derive(Debug, Clone)]
pub struct FloorItem {
    entity_id: ActiveItemEntityId,
    pub item_id: ClientItemId,
    pub item: FloorItemType,
    pub map_area: MapArea,
    pub x: f32,
    pub y: f32,
    pub z: f32,
}

#[derive(Debug)]
pub struct BankItem {
    id: ActiveItemId,
    item: HeldItemType,
}


#[derive(Debug)]
pub struct CharacterInventory<'a>(&'a Vec<InventoryItem>);

impl<'a> CharacterInventory<'a> {
    pub fn as_client_inventory_items(&self) -> [character::InventoryItem; 30] {
        self.0.iter()
            .enumerate()
            .fold([character::InventoryItem::default(); 30], |mut inventory, (slot, item)| {
                let bytes = item.item.as_client_bytes();
                inventory[slot].data1.copy_from_slice(&bytes[0..12]);
                inventory[slot].data2.copy_from_slice(&bytes[12..16]);
                inventory[slot].item_id = item.item_id.0;
                // does this do anything?
                inventory[slot].equipped = if item.equipped { 1 } else { 0 };
                // because this actually equips the item
                inventory[slot].flags |= if item.equipped { 8 } else { 0 };
                inventory
            })
    }

    pub fn slot(&self, slot: usize) -> Option<&'a InventoryItem> {
        self.0.get(slot)
    }

    pub fn count(&self) -> usize {
        self.0.len()
    }
}

#[derive(Error, Debug)]
#[error("")]
pub enum ItemManagerError {
    EntityGatewayError,
    NoSuchItemId(ClientItemId),
    NoCharacter(CharacterEntityId),
    CouldNotAddToInventory(FloorItem),
    //ItemBelongsToOtherPlayer,
    Idunnoman,
    CouldNotSplitItem(InventoryItem),
    CouldNotDropMeseta,
}

pub struct ItemManager {
    id_counter: u32,

    character_inventory: HashMap<CharacterEntityId, Vec<InventoryItem>>,
    character_floor: HashMap<CharacterEntityId, Vec<FloorItem>>,

    character_room: HashMap<CharacterEntityId, RoomId>,
    room_floor: HashMap<RoomId, Vec<FloorItem>>,
    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_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 fn load_character<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) {
        let items = entity_gateway.get_items_by_character(&character);
        let inventory_items = items.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 stacked = acc.entry(slot).or_insert((HeldItemType::Stacked(tool, 0), ActiveItemEntityId::Stacked(Vec::new()), false));
                        if let HeldItemType::Stacked(_, ref mut item_count) = stacked.0 {
                            *item_count += 1;
                        }
                        if let ActiveItemEntityId::Stacked(ref mut id_list) = stacked.1 {
                            id_list.push(id);
                        }
                    }
                }
                else {
                    acc.insert(slot, (HeldItemType::Individual(item), ActiveItemEntityId::Individual(id), equipped));
                }

                acc
            })
            .into_iter()
            .map(|(_slot, (held_item, entity_id, equipped))| {
                let id = self.next_global_item_id();
                InventoryItem {
                    entity_id: entity_id,
                    item_id: id,
                    item: held_item,
                    equipped: equipped,
                }
            });
        let k = inventory_items.take(30).collect();
        self.character_inventory.insert(character.id, k);
    }

    pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) {
        let base_id = ((area_client.local_client.id() as u32) << 21) | 0x10000;
        let inventory = self.character_inventory.get_mut(&character.id).unwrap();
        for (i, item) in inventory.iter_mut().enumerate() {
            item.item_id = ClientItemId(base_id + i as u32);
        }
        self.character_room.insert(character.id, room_id);
        self.character_floor.insert(character.id, Vec::new());
        self.room_floor.entry(room_id).or_insert(Vec::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(CharacterInventory(self.character_inventory.get(&character.id)
                              .ok_or(ItemManagerError::NoCharacter(character.id))?))
    }

    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_inventory_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<InventoryItem, ItemManagerError> {
        let inventory = self.character_inventory.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        inventory.iter()
            .filter(|item| {
                item.item_id == item_id
            })
            .nth(0)
            .ok_or(ItemManagerError::NoSuchItemId(item_id))
            .map(Clone::clone)
    }

    pub fn get_floor_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<FloorItem, ItemManagerError> {
        let 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))?;
        floor.iter()
            .chain(shared_floor.iter())
            .filter(|item| {
                item.item_id == item_id
            })
            .nth(0)
            .ok_or(ItemManagerError::NoSuchItemId(item_id))
            .map(Clone::clone)
    }

    pub fn character_picks_up_item<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, floor_item: FloorItem) -> Result<(), 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))?;

        match &floor_item.item {
            FloorItemType::Individual(_item) => {
                if inventory.len() >= 30 {
                    return Err(ItemManagerError::CouldNotAddToInventory(floor_item));
                }
            },
            FloorItemType::Stacked(floor_tooltype, floor_amount) => {
                let tool_overflow = inventory.iter()
                    .find(|item| {
                        if let HeldItemType::Stacked(inv_tooltype, inv_amount) = item.item {
                            if floor_tooltype.tool == inv_tooltype.tool {
                                if floor_tooltype.tool.max_stack() < (inv_amount + floor_amount) {
                                    return true
                                }
                            }
                        }
                        false
                    });
                if tool_overflow.is_some() {
                    return Err(ItemManagerError::CouldNotAddToInventory(floor_item));
                }
            },
            FloorItemType::Meseta(_meseta) => {
                if character.meseta == 999999 {
                    return Err(ItemManagerError::CouldNotAddToInventory(floor_item));
                }
            },
        }

        if let Some(_) = local_floor.iter().find(|i| i.item_id == floor_item.item_id) {
            local_floor.retain(|item| {
                item.item_id != floor_item.item_id
            });
        }
        else if let Some(_) = shared_floor.iter().find(|i| i.item_id == floor_item.item_id) {
            shared_floor.retain(|item| {
                item.item_id != floor_item.item_id
            });
        }
        else {
            return Err(ItemManagerError::NoSuchItemId(floor_item.item_id))
        }

        match floor_item.item {
            FloorItemType::Individual(item) => {
                let inventory_item = InventoryItem {
                    entity_id: floor_item.entity_id,
                    item_id: floor_item.item_id,
                    item: HeldItemType::Individual(item.clone()),
                    equipped: false,
                };

                if let ActiveItemEntityId::Individual(item_id) = &inventory_item.entity_id {
                    entity_gateway.save_item(&ItemEntity {
                        id: *item_id,
                        item: item,
                        location: ItemLocation::Inventory {
                            character_id: character.id,
                            slot: inventory.len(),
                            equipped: false,
                        },
                    }); // TODO: error check
                    inventory.push(inventory_item);
                } // else something went very wrong TODO: log it
            },
            FloorItemType::Stacked(tool, amount) => {
                let inventory_item = inventory.iter_mut()
                    .filter(|i| {
                        if let HeldItemType::Stacked(tooltype, _amount) = i.item {
                            tooltype == tool
                        }
                        else {
                            false
                        }
                    })
                    .next()
                    .map(|existing_inv_item| {
                        if let (ActiveItemEntityId::Stacked(ref mut inv_item_id),
                                ActiveItemEntityId::Stacked(floor_item_id))
                            = (&mut existing_inv_item.entity_id, &floor_item.entity_id)
                        {
                            inv_item_id.append(&mut floor_item_id.clone());
                        }

                        if let (HeldItemType::Stacked(_inv_tooltype, ref mut inv_amount),
                                FloorItemType::Stacked(_floor_tooltype, floor_amount))
                            = (&mut existing_inv_item.item, &floor_item.item)
                        {
                            // TODO: check tools are eq?
                            *inv_amount += floor_amount
                        }

                        existing_inv_item.clone()
                    })
                    .unwrap_or_else(|| {
                        let picked_up_item = InventoryItem {
                            entity_id: floor_item.entity_id,
                            item_id: floor_item.item_id,
                            item: HeldItemType::Stacked(tool, amount),
                            equipped: false,
                        };
                        inventory.push(picked_up_item.clone());
                        picked_up_item
                    });

                if let ActiveItemEntityId::Stacked(item_ids) = &inventory_item.entity_id {
                    for item_id in item_ids {
                        entity_gateway.save_item(&ItemEntity {
                            id: *item_id,
                            item: ItemDetail::Tool(tool),
                            location: ItemLocation::Inventory {
                                character_id: character.id,
                                slot: inventory.len(),
                                equipped: false,
                            },
                        }); // TODO: error check
                    };
                } // else something went very wrong TODO: log it
            },
            FloorItemType::Meseta(meseta) => {
                character.meseta = std::cmp::min(character.meseta + meseta.0, 999999);
                entity_gateway.save_character(&character);
            }
        }

        Ok(())
    }

    pub fn enemy_drop_item_on_local_floor<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, ItemManagerError> {
        let item = match item_drop.item {
            ItemDropType::Weapon(w) => FloorItemType::Individual(ItemDetail::Weapon(w)),
            ItemDropType::Armor(w) => FloorItemType::Individual(ItemDetail::Armor(w)),
            ItemDropType::Shield(w) => FloorItemType::Individual(ItemDetail::Shield(w)),
            ItemDropType::Unit(w) => FloorItemType::Individual(ItemDetail::Unit(w)),
            ItemDropType::TechniqueDisk(w) => FloorItemType::Individual(ItemDetail::TechniqueDisk(w)),
            ItemDropType::Mag(w) => FloorItemType::Individual(ItemDetail::Mag(w)),
            ItemDropType::Tool(w) => FloorItemType::Individual(ItemDetail::Tool(w)),
            //ItemDropType::Tool(t) if t.is_stackable() => FloorItemType::Stacked(t, ),
            //ItemDropType::Tool(t) if !t.is_stackable() => FloorItemType::Individual(ItemDetail::Tool(w)),
            ItemDropType::Meseta(m) => FloorItemType::Meseta(Meseta(m))
        };

        let entity_id = match &item {
            FloorItemType::Individual(i) => {
                let entity = entity_gateway.create_item(NewItemEntity {
                    item: i.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,
                    }
                }).ok_or(ItemManagerError::EntityGatewayError)?;
                ActiveItemEntityId::Individual(entity.id)
            },
            FloorItemType::Stacked(tool, count) => {
                let entities = (0..*count).map(|_| {
                    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,
                        }
                    })})
                    .map(|entity| -> Result<ItemEntityId, ItemManagerError> {
                        let e = entity.ok_or(ItemManagerError::EntityGatewayError)?;
                        Ok(e.id)
                    });
                ActiveItemEntityId::Stacked(entities.collect::<Result<Vec<_>, _>>()?)
            },
            FloorItemType::Meseta(m) => ActiveItemEntityId::Meseta(m.clone()),
        };

        let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
        let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?();

        let floor_item = FloorItem {
            entity_id: entity_id,
            item_id: item_id,
            item: item,
            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(Vec::new()).push(floor_item);
        self.character_floor.get(&character.id).ok_or(ItemManagerError::Idunnoman)?.last().ok_or(ItemManagerError::Idunnoman)
    }

    pub fn player_drop_item_on_shared_floor<EG: EntityGateway>(&mut self,
                                                               entity_gateway: &mut EG,
                                                               character: &CharacterEntity,
                                                               inventory_item: InventoryItem,
                                                               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))?;

        inventory
            .drain_filter(|i| i.item_id == inventory_item.item_id)
            .nth(0)
            .ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id))?;

        let room_floor_item = FloorItem {
            entity_id: inventory_item.entity_id,
            item_id: inventory_item.item_id,
            item: match inventory_item.item {
                HeldItemType::Individual(item) => FloorItemType::Individual(item),
                HeldItemType::Stacked(tool, count) => FloorItemType::Stacked(tool, count),
            },
            map_area: item_drop_location.0,
            x: item_drop_location.1,
            y: item_drop_location.2,
            z: item_drop_location.3,
        };

        match &room_floor_item.item {
            FloorItemType::Individual(item) => {
                if let ActiveItemEntityId::Individual(item_id) = &room_floor_item.entity_id {
                    entity_gateway.save_item(&ItemEntity {
                        id: *item_id,
                        item: item.clone(),
                        location: ItemLocation::SharedFloor {
                            map_area: item_drop_location.0,
                            x: item_drop_location.1,
                            y: item_drop_location.2,
                            z: item_drop_location.3,
                        }
                    }); // TODO: error check
                } // else something went very wrong: TODO: log it 
            },
            FloorItemType::Stacked(tool, _count) => {
                if let ActiveItemEntityId::Stacked(item_ids) = &room_floor_item.entity_id {
                    for item_id in item_ids {
                        entity_gateway.save_item(&ItemEntity {
                            id: *item_id,
                            item: ItemDetail::Tool(*tool),
                            location: ItemLocation::SharedFloor {
                                map_area: item_drop_location.0,
                                x: item_drop_location.1,
                                y: item_drop_location.2,
                                z: item_drop_location.3,
                            },
                        }); // TODO: error check
                    }
                } // else something went very wrong TODO: log it
            },
            _ => {}, // can meseta get here?
        }

        shared_floor.push(room_floor_item);
        Ok(())
    }

    pub 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);

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

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

    pub fn player_drops_partial_stack_on_shared_floor<EG: EntityGateway>(&mut self,
                                                                         entity_gateway: &mut EG,
                                                                         character: &CharacterEntity,
                                                                         inventory_item: InventoryItem,
                                                                         drop_location: ItemDropLocation,
                                                                         amount: usize)
                                                                        -> Result<FloorItem, 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.iter_mut()
            .find(|i| i.item_id == inventory_item.item_id)
            .ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id))?;

        if let (ActiveItemEntityId::Stacked(ref mut entity_ids), HeldItemType::Stacked(tool, ref mut tool_amount)) = (&mut item_to_split.entity_id, &mut item_to_split.item) {
            if entity_ids.len() <= amount || *tool_amount <= amount {
                return Err(ItemManagerError::CouldNotSplitItem(inventory_item));
            }

            *tool_amount -= amount;

            let dropped_entities = entity_ids.drain(..amount).collect::<Vec<_>>();

            dropped_entities.iter().for_each(|entity_id| {
                entity_gateway.save_item(&ItemEntity {
                    id: *entity_id,
                    item: ItemDetail::Tool(*tool),
                    location: ItemLocation::SharedFloor {
                        map_area: drop_location.map_area,
                        x: drop_location.x,
                        y: 0.0,
                        z: drop_location.z,
                    }
                })
            });

            let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?();
            let floor_item = FloorItem {
                entity_id: ActiveItemEntityId::Stacked(dropped_entities),
                item_id: item_id,
                item: FloorItemType::Stacked(*tool, amount),
                map_area: drop_location.map_area,
                x: drop_location.x,
                y: 0.0,
                z: drop_location.z,
            };
            shared_floor.push(floor_item.clone());
            Ok(floor_item)
        }
        else {
            Err(ItemManagerError::CouldNotSplitItem(inventory_item))
        }
    }
}