use log::warn;
use rand::Rng;
use rand::seq::SliceRandom;
use libpso::packet::ship::*;
use libpso::packet::messages::*;
use crate::common::leveltable::LEVEL_TABLE;
use crate::common::serverstate::ClientId;
use crate::ship::ship::{SendShipPacket, ShipError, Clients, ItemShops};
use crate::ship::location::ClientLocation;
use crate::ship::drops::ItemDrop;
use crate::ship::room::Rooms;
use crate::ship::items::ClientItemId;
use crate::entity::gateway::EntityGateway;
use crate::entity::item;
use libpso::utf8_to_utf16_array;
use crate::ship::packet::builder;
use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem};
use crate::ship::items::state::{ItemState, ItemStateError};
use crate::ship::items::floor::{FloorType, FloorItemDetail};
use crate::ship::items::actions::TriggerCreateItem;
use crate::ship::items::tasks::{pick_up_item, withdraw_meseta, deposit_meseta, withdraw_item, deposit_item, buy_shop_item, enemy_drops_item, take_meseta, apply_modifier};

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;

#[derive(thiserror::Error, Debug)]
pub enum MessageError {
    #[error("invalid tek {0}")]
    InvalidTek(ClientItemId),
    #[error("mismatched tek {0} {1}")]
    MismatchedTekIds(ClientItemId, ClientItemId),
}

async fn send_to_client(id: ClientId,
                        target: u8,
                        msg: DirectMessage,
                        client_location: &ClientLocation)
                        -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error> {
    Ok(client_location.get_all_clients_by_client(id)
       .await?
       .into_iter()
       .filter(move |client| client.local_client.id() == target)
       .map(move |client| {
           (client.client, SendShipPacket::DirectMessage(msg.clone()))
       })
       .collect())
}

pub async fn guildcard_send(id: ClientId,
                            guildcard_send: GuildcardSend,
                            target: u32,
                            client_location: &ClientLocation,
                            clients: &Clients)
                            -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error> {
    let msg = clients.with(id, |client| Box::pin(async move {
        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(),
            }),
        }
    })).await?;

    send_to_client(id, target as u8, msg, client_location).await
}

pub async fn request_item<EG>(id: ClientId,
                              request_item: RequestItem,
                              entity_gateway: &mut EG,
                              client_location: &ClientLocation,
                              clients: &Clients,
                              rooms: &Rooms,
                              item_state: &mut ItemState)
                              -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + 'static,
{
    let room_id = client_location.get_room(id).await?;
    let monster = rooms.with(room_id, |room| Box::pin(async move {
        room.maps.enemy_by_id(request_item.enemy_id as usize)
    })).await??;

    if monster.dropped_item {
        return Err(ShipError::MonsterAlreadyDroppedItem(id, request_item.enemy_id).into())
    }

    let clients_in_area = client_location.get_clients_in_room(room_id).await?;
    let client_and_drop = rooms.with_mut(room_id, |room| Box::pin(async move {
        clients_in_area.into_iter()
            .filter_map(move |area_client| {
                room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| {
                    (area_client, item_drop_type)
                })
            })
            .collect::<Vec<_>>()
    })).await?;
    
    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 character_id = clients.with(id, |client| Box::pin(async move {
            client.character.id
        })).await?;

        let floor_item = enemy_drops_item(item_state, entity_gateway, character_id, 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(item_drop_packets)
}

pub async fn pickup_item<EG>(id: ClientId,
                                 pickup_item: PickupItem,
                                 entity_gateway: &mut EG,
                                 client_location: &ClientLocation,
                                 clients: &Clients,
                                 item_state: &mut ItemState)
                                 -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    let area_client = client_location.get_local_client(id).await?;
    let room_id = client_location.get_room(id).await?;
    let clients_in_area = client_location.get_clients_in_room(room_id).await?;

    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            let (item, floor_type) = item_state.get_floor_item(&client.character.id, &ClientItemId(pickup_item.item_id)).await?;
            let remove_item = builder::message::remove_item_from_floor(area_client, &item)?;
            let create_item = match &item.item {
                FloorItemDetail::Individual(individual_floor_item) => Some(builder::message::create_individual_item(area_client, item.item_id, individual_floor_item)?),
                FloorItemDetail::Stacked(stacked_floor_item) => Some(builder::message::create_stacked_item(area_client, item.item_id, &stacked_floor_item.tool, stacked_floor_item.count())?),
                FloorItemDetail::Meseta(_) => None,
            };

            match pick_up_item(&mut item_state, &mut entity_gateway, &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(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
                                  }
                              }))
                       .collect())
                },
                Err(err) => {
                    warn!("character {:?} could not pick up item: {:?}", client.character.id, err);
                    Ok(Vec::new())
                },
            }
        })}).await?
}

pub async fn request_box_item<EG>(id: ClientId,
                                  box_drop_request: BoxDropRequest,
                                  entity_gateway: &mut EG,
                                  client_location: &ClientLocation,
                                  clients: &Clients,
                                  rooms: &Rooms,
                                  item_state: &mut ItemState)
                                  -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static
{
    let room_id = client_location.get_room(id).await?;
    let box_object = rooms.with(room_id, |room| Box::pin(async move {
        room.maps.object_by_id(box_drop_request.object_id as usize)
    })).await??;

    if box_object.dropped_item {
        return Err(ShipError::BoxAlreadyDroppedItem(id, box_drop_request.object_id).into())
    }

    let clients_in_area = client_location.get_clients_in_room(room_id).await?;
    
    let client_and_drop = rooms.with_mut(room_id, |room| Box::pin(async move {
        clients_in_area.into_iter()
            .filter_map(move |area_client| {
                room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| {
                    (area_client, item_drop_type)
                })
            })
            .collect::<Vec<_>>()
    })).await?;

    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 character_id = clients.with(area_client.client, |client| Box::pin(async move {
            client.character.id
        })).await?;
        let floor_item = enemy_drops_item(item_state, entity_gateway, character_id, item_drop).await?;
        //let floor_item = enemy_drops_item(item_state, &mut entity_gateway, client.character.id, item_drop).await?;
        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(item_drop_packets)
}


pub async fn send_bank_list(id: ClientId,
                            clients: &Clients,
                            item_state: &mut ItemState)
                            -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
    let bank = clients.with(id, |client| {
        let item_state = item_state.clone();
        Box::pin(async move {
            item_state.get_character_bank(&client.character).await
        })
    }).await??;
    let bank_items_pkt = builder::message::bank_item_list(&bank);
    Ok(vec![(id, SendShipPacket::BankItemList(bank_items_pkt))])
}

pub async fn bank_interaction<EG>(id: ClientId,
                                  bank_interaction: BankInteraction,
                                  entity_gateway: &mut EG,
                                  client_location: &ClientLocation,
                                  clients: &Clients,
                                  item_state: &mut ItemState)
                                  -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    let area_client = client_location.get_local_client(id).await?;
    let other_clients_in_area = client_location.get_all_clients_by_client(id).await?;
    
    let bank_action_pkts = clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            Ok::<_, anyhow::Error>(match bank_interaction.action {
                BANK_ACTION_DEPOSIT => {
                    if bank_interaction.item_id == 0xFFFFFFFF {
                        deposit_meseta(&mut item_state, &mut entity_gateway, &client.character, bank_interaction.meseta_amount).await?;
                        Vec::new()
                    }
                    else {
                        deposit_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32).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 {
                        withdraw_meseta(&mut item_state, &mut entity_gateway, &client.character, bank_interaction.meseta_amount).await?;
                        Vec::new()
                    }
                    else {
                        let item_added_to_inventory = withdraw_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32).await?;
                        let item_created = builder::message::create_withdrawn_inventory_item2(area_client, &item_added_to_inventory)?;
                        vec![SendShipPacket::Message(Message::new(GameMessage::CreateItem(item_created)))]
                    }
                },
                _ => { // TODO: error?
                    Vec::new()
                }
            })
        })
    }).await??;
                                        
    Ok(other_clients_in_area.into_iter()
       .flat_map(move |c| {
           bank_action_pkts.clone().into_iter()
               .map(move |pkt| {
                   (c.client, pkt)
               })
       })
       .collect()
    )
}

pub async fn shop_request(id: ClientId,
                          shop_request: ShopRequest,
                          client_location: &ClientLocation,
                          clients: &Clients,
                          rooms: &Rooms,
                          shops: &ItemShops)
                          -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
    //let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
    let room_id = client_location.get_room(id).await?;
    /*
    let room = rooms.get(room_id.0)
        .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?
        .as_ref()
        .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?;
     */
    let difficulty = rooms.with(room_id, |room| Box::pin(async move {
        room.mode.difficulty()
    })).await?;
    let shop_list = clients.with_mut(id, |client| {
        let mut shops = shops.clone();
        Box::pin(async move {
            let level = LEVEL_TABLE.get_level_from_exp(client.character.char_class, client.character.exp) as usize;
            match shop_request.shop_type {
                SHOP_OPTION_WEAPON => {
                    client.weapon_shop = shops.weapon_shop.get_mut(&(difficulty, client.character.section_id))
                        .ok_or(ShipError::ShopError)?
                        .lock()
                        .await
                        .generate_weapon_list(level);
                    Ok(builder::message::shop_list(shop_request.shop_type, &client.weapon_shop))
                },
                SHOP_OPTION_TOOL => {
                    client.tool_shop = shops.tool_shop
                        .lock()
                        .await
                        .generate_tool_list(level);
                    Ok(builder::message::shop_list(shop_request.shop_type, &client.tool_shop))
                },
                SHOP_OPTION_ARMOR => {
                    client.armor_shop = shops.armor_shop
                        .lock()
                        .await
                        .generate_armor_list(level);
                    Ok(builder::message::shop_list(shop_request.shop_type, &client.armor_shop))
                },
                _ => {
                    Err(ShipError::ShopError)
                }
            }
        })}).await??;
    /*
    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.into())
        }
    };
     */

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


pub async fn buy_item<EG>(id: ClientId,
                          buy_item: BuyItem,
                          entity_gateway: &mut EG,
                          client_location: &ClientLocation,
                          clients: &Clients,
                          item_state: &mut ItemState)
                          -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    let area_client = client_location.get_local_client(id).await?;

    let create = clients.with_mut(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            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 = matches!(item, ToolShopItem::Tech(_));
                    (item, remove)
                },
                SHOP_OPTION_ARMOR => {
                    let item = client.armor_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?;
                    let remove = matches!(item, ArmorShopItem::Unit(_));
                    (item, remove)
                },
                _ => {
                    return Err(ShipError::ShopError.into())
                }
            };

            let inventory_item = buy_shop_item(&mut item_state, &mut entity_gateway, &client.character, item, ClientItemId(buy_item.item_id), buy_item.amount as u32).await?;

            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);
                    },
                    _ => {}
                }
            }
            Ok::<_, anyhow::Error>(builder::message::create_withdrawn_inventory_item(area_client, &inventory_item)?)
        })}).await??;

    let other_clients_in_area = client_location.get_client_neighbors(id).await?;
    Ok(other_clients_in_area.into_iter()
       .map(move |c| {
           (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create.clone()))))
       })
       .collect())
}


const TEK_SPECIAL_MODIFIER: [item::weapon::TekSpecialModifier; 3] = [item::weapon::TekSpecialModifier::Plus,
                                                                     item::weapon::TekSpecialModifier::Neutral,
                                                                     item::weapon::TekSpecialModifier::Minus];
const TEK_PERCENT_MODIFIER: [item::weapon::TekPercentModifier; 5] = [item::weapon::TekPercentModifier::PlusPlus,
                                                                     item::weapon::TekPercentModifier::Plus,
                                                                     item::weapon::TekPercentModifier::Neutral,
                                                                     item::weapon::TekPercentModifier::Minus,
                                                                     item::weapon::TekPercentModifier::MinusMinus];

pub async fn request_tek_item<EG>(id: ClientId,
                                  tek_request: TekRequest,
                                  entity_gateway: &mut EG,
                                  clients: &Clients,
                                  item_state: &mut ItemState)
                                  -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    //let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;

    // TODO: secids have different mod rates
    let (grind_mod, special_mod, percent_mod) = {
        let mut rng = rand::thread_rng();

        let grind_mod = rng.gen_range(-4, 4);
        let special_mod = TEK_SPECIAL_MODIFIER.choose(&mut rng).cloned().unwrap();
        let percent_mod = TEK_PERCENT_MODIFIER.choose(&mut rng).cloned().unwrap();
        (grind_mod, special_mod, percent_mod)
    };

    let preview_pkt = clients.with_mut(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            client.tek = Some((ClientItemId(tek_request.item_id), special_mod, percent_mod, grind_mod));

            let inventory = item_state.get_character_inventory(&client.character).await?;
            let item = inventory.get_by_client_id(&ClientItemId(tek_request.item_id))
                .ok_or_else(|| ItemStateError::WrongItemType(ClientItemId(tek_request.item_id)))?;
            let mut weapon = *item.item.as_individual()
                .ok_or_else(|| ItemStateError::WrongItemType(ClientItemId(tek_request.item_id)))?
                .as_weapon()
                .ok_or_else(|| ItemStateError::WrongItemType(ClientItemId(tek_request.item_id)))?;

            weapon.apply_modifier(&item::weapon::WeaponModifier::Tekked {
                special: special_mod,
                percent: percent_mod,
                grind: grind_mod,
            });

            take_meseta(&mut item_state, &mut entity_gateway, &client.character.id, item::Meseta(100)).await?;
            Ok::<_, anyhow::Error>(builder::message::tek_preview(ClientItemId(tek_request.item_id), &weapon)?)
        })}).await??;


    Ok(vec![(id, SendShipPacket::Message(Message::new(GameMessage::TekPreview(preview_pkt))))])
}

pub async fn accept_tek_item<EG>(id: ClientId,
                                 tek_accept: TekAccept,
                                 entity_gateway: &mut EG,
                                 client_location: &ClientLocation,
                                 clients: &Clients,
                                 item_state: &mut ItemState)
                                 -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    let area_client = client_location.get_local_client(id).await?;
    let neighbors = client_location.get_client_neighbors(id).await?;

    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
        if let Some((item_id, special_mod, percent_mod, grind_mod)) = client.tek {
            if item_id.0 != tek_accept.item_id {
                return Err(MessageError::MismatchedTekIds(item_id, ClientItemId(tek_accept.item_id)).into());
            }

            let modifier = item::weapon::WeaponModifier::Tekked {
                special: special_mod,
                percent: percent_mod,
                grind: grind_mod,
            };
            let weapon = apply_modifier(&mut item_state, &mut entity_gateway, &client.character, item_id, item::ItemModifier::WeaponModifier(modifier)).await?;

            let create_item_pkt = builder::message::create_individual_item(area_client, item_id, &weapon)?;

            Ok(neighbors.into_iter()
               .map(move |c| {
                   (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create_item_pkt.clone()))))
               })
               .collect())
        }
        else {
            Err(MessageError::InvalidTek(ClientItemId(tek_accept.item_id)).into())
        }
        })}).await?
}