use log::warn;
use libpso::packet::ship::*;
use libpso::packet::messages::*;
use crate::common::leveltable::CharacterLevelTable;
use crate::common::serverstate::ClientId;
use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops};
use crate::ship::location::{ClientLocation, ClientLocationError};
use crate::ship::drops::ItemDrop;
use crate::ship::items::{ItemManager, ClientItemId, TriggerCreateItem, FloorItem, FloorType};
use crate::entity::gateway::EntityGateway;
use libpso::utf8_to_utf16_array;
use crate::ship::packet::builder;
use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem};

const BANK_ACTION_DEPOSIT: u8 = 0;
const BANK_ACTION_WITHDRAW: u8 = 1;

const SHOP_OPTION_TOOL: u8 = 0;
const SHOP_OPTION_WEAPON: u8 = 1;
const SHOP_OPTION_ARMOR: u8 = 2;

const INVENTORY_MESETA_CAPACITY: u32 = 999999;
const BANK_MESETA_CAPACITY: u32 = 999999;

//const BANK_ACTION_: u8 = 1;

fn send_to_client(id: ClientId, target: u8, msg: DirectMessage, client_location: &ClientLocation)
                  -> Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send> {
    Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
             .filter(move |client| client.local_client.id() == target)
             .map(move |client| {
                 (client.client, SendShipPacket::DirectMessage(msg.clone()))
             }))
}

pub fn guildcard_send(id: ClientId,
                      guildcard_send: &GuildcardSend,
                      target: u32,
                      client_location: &ClientLocation,
                      clients: &Clients)
                      -> Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send> {
    let client = clients.get(&id).unwrap();
    let msg = DirectMessage{
        flag: target,
        msg: GameMessage::GuildcardRecv(GuildcardRecv {
            client: guildcard_send.client,
            target: guildcard_send.target,
            guildcard: client.user.id.0,
            name: utf8_to_utf16_array!(client.character.name, 0x18),
            team: [0; 0x10], // TODO: teams not yet implemented
            desc: utf8_to_utf16_array!(client.character.guildcard.description, 0x58),
            one: 1,
            language: 0, // TODO: add language flag to character
            section_id: client.character.section_id.into(),
            class: client.character.char_class.into(),
        }),
    };
    send_to_client(id, target as u8, msg, &client_location)
}

pub async fn request_item<EG>(id: ClientId,
                              request_item: &RequestItem,
                              entity_gateway: &mut EG,
                              client_location: &ClientLocation,
                              clients: &mut Clients,
                              rooms: &mut Rooms,
                              item_manager: &mut ItemManager)
                              -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
    EG: EntityGateway
{
    let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let room = rooms.get_mut(room_id.0)
        .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
        .as_mut()
        .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;

    let monster = room.maps.enemy_by_id(request_item.enemy_id as usize)?;
    if monster.dropped_item {
        return Err(ShipError::MonsterAlreadyDroppedItem(id, request_item.enemy_id))
    }

    let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?;

    let client_and_drop = clients_in_area.into_iter()
        .filter_map(|area_client| {
            room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| {
                (area_client, item_drop_type)
            })
        });

    let mut item_drop_packets = Vec::new();
    for (area_client, item_drop) in client_and_drop {
        let item_drop = ItemDrop {
            map_area: monster.map_area,
            x: request_item.x,
            y: request_item.y,
            z: request_item.z,
            item: item_drop,
        };
        let client = clients.get_mut(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client))?;
        let floor_item = item_manager.enemy_drop_item_on_local_floor(entity_gateway, &client.character, item_drop).await?;
        let item_drop_msg = builder::message::item_drop(request_item.client, request_item.target, &floor_item)?;

        item_drop_packets.push((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg)))));
    }

    Ok(Box::new(item_drop_packets.into_iter()))
}

pub async fn pickup_item<EG>(id: ClientId,
                             pickup_item: &PickupItem,
                             entity_gateway: &mut EG,
                             client_location: &ClientLocation,
                             clients: &mut Clients,
                             item_manager: &mut ItemManager)
                             -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
    EG: EntityGateway
{
    let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
    let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?;

    // TODO: should not need to fetch the item here to construct this packet
    let (item, floor_type) = item_manager.get_floor_item_by_id(&client.character, ClientItemId(pickup_item.item_id))?;
    let remove_item = builder::message::remove_item_from_floor(area_client, &item)?;
    let create_item = match item {
        FloorItem::Meseta(_) => None,
        _ => Some(builder::message::create_item(area_client, &item)?),
    };

    match item_manager.character_picks_up_item(entity_gateway, &mut client.character, ClientItemId(pickup_item.item_id)).await {
        Ok(trigger_create_item) => {
            let remove_packets: Box<dyn Iterator<Item=(ClientId, SendShipPacket)> + Send> = match floor_type {
                FloorType::Local => {
                    Box::new(vec![(id, SendShipPacket::Message(Message::new(GameMessage::RemoveItemFromFloor(remove_item.clone()))))].into_iter())
                },
                FloorType::Shared => {
                    Box::new(clients_in_area.clone().into_iter()
                        .map(move |c| {
                            (c.client, SendShipPacket::Message(Message::new(GameMessage::RemoveItemFromFloor(remove_item.clone()))))
                        }))
                },
            };

            Ok(Box::new(remove_packets
                        .chain(clients_in_area.into_iter().
                               filter_map(move |c| {
                                   match trigger_create_item {
                                       TriggerCreateItem::Yes => create_item.clone().map(|ci| (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(ci))))),
                                       _ => None
                                   }
                               }
                               ))))
        },
        Err(err) => {
            warn!("character {:?} could not pick up item: {:?}", client.character.id, err);
            Ok(Box::new(None.into_iter()))
        },
    }
}

pub async fn request_box_item<EG>(id: ClientId,
    box_drop_request: &BoxDropRequest,
    entity_gateway: &mut EG,
    client_location: &ClientLocation,
    clients: &mut Clients,
    rooms: &mut Rooms,
    item_manager: &mut ItemManager)
    -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
EG: EntityGateway
{
    let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let room = rooms.get_mut(room_id.0)
        .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
        .as_mut()
        .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;

    let box_object = room.maps.object_by_id(box_drop_request.object_id as usize)?;
    if box_object.dropped_item {
        return Err(ShipError::BoxAlreadyDroppedItem(id, box_drop_request.object_id))
    }

    let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?;

    let client_and_drop = clients_in_area.into_iter()
        .filter_map(|area_client| {
            room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| {
                (area_client, item_drop_type)
            })
        });

    let mut item_drop_packets = Vec::new();
    for (area_client, item_drop) in client_and_drop {
        let item_drop = ItemDrop {
            map_area: box_object.map,
            x: box_drop_request.x,
            y: 0.0,
            z: box_drop_request.z,
            item: item_drop,
        };
        let client = clients.get_mut(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client))?;
        let floor_item = item_manager.enemy_drop_item_on_local_floor(entity_gateway, &client.character, item_drop).await?; // TODO: unwrap
        let item_drop_msg = builder::message::item_drop(box_drop_request.client, box_drop_request.target, &floor_item)?;
        item_drop_packets.push((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg)))))
    }

    Ok(Box::new(item_drop_packets.into_iter()))
}


// item_manager is not mutable in this, but for reasons I don't quite understand it requires the unique access of it to compile here
pub async fn send_bank_list(id: ClientId,
                            clients: &Clients,
                            item_manager: &mut ItemManager)
                            -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
{
    let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
    let bank_items = item_manager.get_character_bank(&client.character)?;

    let bank_items_pkt = builder::message::bank_item_list(&bank_items, client.character.bank_meseta);
    Ok(Box::new(vec![(id, SendShipPacket::BankItemList(bank_items_pkt))].into_iter()))
}

pub async fn bank_interaction<EG>(id: ClientId,
                                  bank_interaction: &BankInteraction,
                                  entity_gateway: &mut EG,
                                  client_location: &ClientLocation,
                                  clients: &mut Clients,
                                  item_manager: &mut ItemManager)
                                  -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
    EG: EntityGateway
{
    let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
    let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let other_clients_in_area = client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let bank_action_pkts = match bank_interaction.action {
        BANK_ACTION_DEPOSIT => {
            if bank_interaction.item_id == 0xFFFFFFFF {
                if client.character.meseta >= bank_interaction.meseta_amount && (bank_interaction.meseta_amount + client.character.bank_meseta) <= BANK_MESETA_CAPACITY {
                    client.character.meseta -= bank_interaction.meseta_amount;
                    client.character.bank_meseta += bank_interaction.meseta_amount;
                    entity_gateway.save_character(&client.character).await?;
                }
                Vec::new()
            }
            else {
                item_manager.player_deposits_item(entity_gateway, &client.character, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as usize).await?;
                let player_no_longer_has_item = builder::message::player_no_longer_has_item(area_client, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32);
                vec![SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)))]
            }
        },
        BANK_ACTION_WITHDRAW => {
            if bank_interaction.item_id == 0xFFFFFFFF {
                if client.character.meseta + bank_interaction.meseta_amount <= INVENTORY_MESETA_CAPACITY {
                    client.character.meseta += bank_interaction.meseta_amount;
                    client.character.bank_meseta -= bank_interaction.meseta_amount;
                    entity_gateway.save_character(&client.character).await?;
                }
                Vec::new()
            }
            else {
                let item_added_to_inventory = item_manager.player_withdraws_item(entity_gateway, &client.character, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as usize).await?;
                let item_created = builder::message::create_withdrawn_inventory_item(area_client, &item_added_to_inventory)?;
                vec![SendShipPacket::Message(Message::new(GameMessage::CreateItem(item_created)))]
            }
        },
        _ => {
            Vec::new()
        }
    };

    Ok(Box::new(other_clients_in_area.into_iter()
                .map(move |c| {
                    bank_action_pkts.clone().into_iter()
                        .map(move |pkt| {
                            (c.client, pkt)
                        })
                })
                .flatten()
    ))
}

pub async fn shop_request(id: ClientId,
                          shop_request: &ShopRequest,
                          client_location: &ClientLocation,
                          clients: &mut Clients,
                          rooms: &Rooms,
                          level_table: &CharacterLevelTable,
                          shops: &mut ItemShops)
                          -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
{
    let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
    let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
    let room = rooms.get(room_id.0)
        .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
        .as_ref()
        .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;
    let level = level_table.get_level_from_exp(client.character.char_class, client.character.exp) as usize;
    let shop_list = match shop_request.shop_type {
        SHOP_OPTION_WEAPON => {
            client.weapon_shop = shops.weapon_shop.get_mut(&(room.mode.difficulty(), client.character.section_id))
                .ok_or(ShipError::ShopError)?
                .generate_weapon_list(level);
            builder::message::shop_list(shop_request.shop_type, &client.weapon_shop)
        },
        SHOP_OPTION_TOOL => {
            client.tool_shop = shops.tool_shop.generate_tool_list(level);
            builder::message::shop_list(shop_request.shop_type, &client.tool_shop)
        },
        SHOP_OPTION_ARMOR => {
            client.armor_shop = shops.armor_shop.generate_armor_list(level);
            builder::message::shop_list(shop_request.shop_type, &client.armor_shop)
        },
        _ => {
            return Err(ShipError::ShopError)
        }
    };

    Ok(Box::new(vec![(id, SendShipPacket::Message(Message::new(GameMessage::ShopList(shop_list))))].into_iter()))
}


pub async fn buy_item<EG>(id: ClientId,
                          buy_item: &BuyItem,
                          entity_gateway: &mut EG,
                          client_location: &ClientLocation,
                          clients: &mut Clients,
                          item_manager: &mut ItemManager)
                          -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
    EG: EntityGateway
{
    let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
    let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?;


    let (item, remove): (&(dyn ShopItem + Send + Sync), bool) = match buy_item.shop_type {
        SHOP_OPTION_WEAPON => {
            (client.weapon_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?, false)
        },
        SHOP_OPTION_TOOL => {
            let item = client.tool_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?;
            let remove = match item {
                ToolShopItem::Tech(_) => true,
                _ => false,
            };
            (item, remove)
        },
        SHOP_OPTION_ARMOR => {
            let item = client.armor_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?;
            let remove = match item {
                ArmorShopItem::Unit(_) => true,
                _ => false,
            };
            (item, remove)
        },
        _ => {
            return Err(ShipError::ShopError)
        }
    };

    if client.character.meseta < item.price() as u32 {
        return Err(ShipError::ShopError)
    }

    client.character.meseta -= item.price() as u32;
    entity_gateway.save_character(&client.character).await?;

    let inventory_item = item_manager.player_buys_item(entity_gateway, &client.character, item, ClientItemId(buy_item.item_id), buy_item.amount as usize).await?;
    let create = builder::message::create_withdrawn_inventory_item(area_client, inventory_item)?;

    if remove {
        match buy_item.shop_type {
            SHOP_OPTION_TOOL => {
                client.tool_shop.remove(buy_item.shop_index as usize);
            },
            SHOP_OPTION_ARMOR => {
                client.armor_shop.remove(buy_item.shop_index as usize);
            },
            _ => {}
        }
    }

    let other_clients_in_area = client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError { err.into() })?;
    Ok(Box::new(other_clients_in_area.into_iter()
                .map(move |c| {
                    (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create.clone()))))
                })))

}