use libpso::packet::ship::*;
use libpso::packet::messages::*;
use crate::entity::gateway::EntityGateway;
use crate::entity::item::Meseta;
use crate::common::serverstate::ClientId;
use crate::common::leveltable::LEVEL_TABLE;
use crate::ship::ship::{SendShipPacket, ShipError, Clients, ItemDropLocation};
use crate::ship::room::Rooms;
use crate::ship::location::{ClientLocation, ClientLocationError};
use crate::ship::items::ClientItemId;
use crate::ship::packet::builder;
use crate::ship::items::state::ItemState;
use crate::ship::items::tasks::{drop_item, drop_partial_item, drop_meseta, equip_item, unequip_item, sort_inventory, use_item, feed_mag, sell_item, take_meseta};

pub async fn request_exp<EG>(id: ClientId,
                             request_exp: RequestExp,
                             entity_gateway: &mut EG,
                             client_location: &ClientLocation,
                             clients: &Clients,
                             rooms: &Rooms)
                             -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    let area_client = client_location.get_local_client(id).await.map_err(|err| -> ClientLocationError { err.into() })?;
    let room_id = client_location.get_room(id).await.map_err(|err| -> ClientLocationError { err.into() })?;

    let enemy_id = request_exp.enemy_id as usize;
    let enemy_exp = rooms.with(room_id, |room| Box::pin(async move {
        let monster = room.maps.enemy_by_id(enemy_id)?;
        let monster_stats = room.monster_stats.get(&monster.monster).ok_or_else(|| ShipError::UnknownMonster(monster.monster))?;
        Ok::<_, ShipError>(monster_stats.exp)
    })).await??;

    let exp_gain = if request_exp.last_hitter == 1 {
        enemy_exp
    }
    else {
        ((enemy_exp as f32) * 0.8) as u32
    };

    let clients_in_area = client_location.get_clients_in_room(room_id).await.map_err(|err| -> ClientLocationError { err.into() })?;
    let gain_exp_pkt = builder::message::character_gained_exp(area_client, exp_gain);
    let mut exp_pkts: Vec<_> = clients_in_area.clone().into_iter()
        .map(move |c| {
            (c.client, SendShipPacket::Message(Message::new(GameMessage::GiveCharacterExp(gain_exp_pkt.clone()))))
        })
        .collect();

    let (char_class, exp) = clients.with(id, |client| Box::pin(async move {
        (client.character.char_class, client.character.exp)
    })).await?;
    
    let before_level = LEVEL_TABLE.get_level_from_exp(char_class, exp);
    let after_level = LEVEL_TABLE.get_level_from_exp(char_class, exp + exp_gain);
    let level_up = before_level != after_level;

    if level_up {
        let (_, before_stats) = LEVEL_TABLE.get_stats_from_exp(char_class, exp);
        let (after_level, after_stats) = LEVEL_TABLE.get_stats_from_exp(char_class, exp + exp_gain);

        let level_up_pkt = builder::message::character_leveled_up(area_client, after_level, before_stats, after_stats);
        exp_pkts.extend(clients_in_area.into_iter()
                        .map(move |c| {
                            (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerLevelUp(level_up_pkt.clone()))))
                        }));
    }

    clients.with_mut(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        Box::pin(async move {
            client.character.exp += exp_gain;
            entity_gateway.save_character(&client.character).await
        })}).await??;

    Ok(exp_pkts)
}

pub async fn player_drop_item<EG>(id: ClientId,
                                  player_drop_item: PlayerDropItem,
                                  entity_gateway: &mut EG,
                                  client_location: &ClientLocation,
                                  clients: &Clients,
                                  rooms: &Rooms,
                                  item_state: &mut ItemState)
                            -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    let room_id = client_location.get_room(id).await.map_err(|err| -> ClientLocationError { err.into() })?;
    let map_area = rooms.with(room_id, |room| Box::pin(async move {
        room.map_areas.get_area_map(player_drop_item.map_area)
    })).await??;

    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            drop_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(player_drop_item.item_id), map_area, (player_drop_item.x, player_drop_item.y, player_drop_item.z)).await
        })}).await??;
    let clients_in_area = client_location.get_clients_in_room(room_id).await.map_err(|err| -> ClientLocationError { err.into() })?;
    let pdi = player_drop_item.clone();
    Ok(clients_in_area.into_iter()
       .map(move |c| {
           (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerDropItem(pdi.clone()))))
       })
       .collect())
}

pub async fn drop_coordinates(id: ClientId,
                              drop_coordinates: DropCoordinates,
                              client_location: &ClientLocation,
                              clients: &Clients,
                              rooms: &Rooms)
                              -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
{
    let room_id = client_location.get_room(id).await.map_err(|err| -> ClientLocationError { err.into() })?;
    let map_area = rooms.with(room_id, |room| Box::pin(async move {
        room.map_areas.get_area_map(drop_coordinates.map_area)
    })).await??;

    clients.with_mut(id, |client| Box::pin(async move {
        client.item_drop_location = Some(ItemDropLocation {
            map_area,
            x: drop_coordinates.x,
            z: drop_coordinates.z,
            item_id: ClientItemId(drop_coordinates.item_id),
        });
    })).await?;

    Ok(Vec::new()) // TODO: do we need to send a packet here?
}

pub async fn no_longer_has_item<EG>(id: ClientId,
                                    no_longer_has_item: PlayerNoLongerHasItem,
                                    entity_gateway: &mut EG,
                                    client_location: &ClientLocation,
                                    clients: &Clients,
                                    item_state: &mut ItemState)
                                    -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    //let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
    let area_client = client_location.get_local_client(id).await.map_err(|err| -> ClientLocationError { err.into() })?;
    let room_id = client_location.get_room(id).await.map_err(|err| -> ClientLocationError { err.into() })?;

    let (drop_location, tek) = clients.with(id, |client| Box::pin(async move {
        (client.item_drop_location, client.tek)
    })).await?;
    if let Some(drop_location) = drop_location {
        if drop_location.item_id.0 != no_longer_has_item.item_id {
            return Err(ShipError::DropInvalidItemId(no_longer_has_item.item_id));
        }

        if no_longer_has_item.item_id == 0xFFFFFFFF {
            let dropped_meseta = clients.with_mut(id, |client| {
                let mut entity_gateway = entity_gateway.clone();
                let mut item_state = item_state.clone();
                Box::pin(async move {
                    client.item_drop_location = None;
                    drop_meseta(&mut item_state, &mut entity_gateway, &client.character, drop_location.map_area, (drop_location.x, drop_location.z), no_longer_has_item.amount).await
                })}).await??;

            let dropped_meseta_pkt = builder::message::drop_split_meseta_stack(area_client, &dropped_meseta)?;
            let no_longer_has_meseta_pkt = builder::message::player_no_longer_has_meseta(area_client, no_longer_has_item.amount);

            let clients_in_area = client_location.get_clients_in_room(room_id).await.map_err(|err| -> ClientLocationError { err.into() })?;
            Ok(clients_in_area.into_iter()
               .flat_map(move |c| {
                   std::iter::once((c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_meseta_pkt.clone())))))
                       .chain(
                           if c.client != id {
                               Box::new(std::iter::once(
                                   (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(no_longer_has_meseta_pkt.clone()))))
                               )) as Box<dyn Iterator<Item = _> + Send>
                           }
                           else {
                               Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>
                           }
                       )
               })
               .collect()
            )
        }
        else {
            let dropped_item = clients.with_mut(id, |client| {
                let mut entity_gateway = entity_gateway.clone();
                let mut item_state = item_state.clone();
                Box::pin(async move {
                    client.item_drop_location = None;
                    drop_partial_item(&mut item_state,
                                      &mut entity_gateway,
                                      &client.character,
                                      &drop_location.item_id,
                                      drop_location.map_area,
                                      (drop_location.x, drop_location.z),
                                      no_longer_has_item.amount)
                        .await
                })}).await??;
            let dropped_item_pkt = builder::message::drop_split_stack(area_client, &dropped_item)?;

            let clients_in_area = client_location.get_clients_in_room(room_id).await.map_err(|err| -> ClientLocationError { err.into() })?;
            Ok(clients_in_area.into_iter()
               .map(move |c| {
                   (c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_item_pkt.clone()))))
               })
               .collect())
        }
    }
    else if let Some(_tek) = tek {
        let neighbors = client_location.get_client_neighbors(id).await.map_err(|err| -> ClientLocationError { err.into() })?;
        let no_longer_has_item = no_longer_has_item.clone();
        Ok(neighbors.into_iter()
           .map(move |c| {
               (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(no_longer_has_item.clone()))))
           })
           .collect())
    }
    else {
        Err(ShipError::InvalidItem(ClientItemId(no_longer_has_item.item_id)))
    }
}

pub async fn update_player_position(id: ClientId,
                                    message: Message,
                                    clients: &Clients,
                                    client_location: &ClientLocation,
                                    rooms: &Rooms)
                                    -> Result<Vec<(ClientId, SendShipPacket)>, ShipError> {
    if let Ok(room_id) = client_location.get_room(id).await.map_err(|err| -> ClientLocationError { err.into() }) {
        let msg = message.msg.clone();
        clients.with_mut(id, |client| {
            let rooms = rooms.clone();
            Box::pin(async move {
                match msg {
                    GameMessage::PlayerChangedMap(p) => {
                        client.x = p.x;
                        client.y = p.y;
                        client.z = p.z;
                    },
                    GameMessage::PlayerChangedMap2(p) => {
                        client.area = rooms.with(room_id, |room| Box::pin(async move {
                            room.map_areas.get_area_map(p.map_area).ok()
                        })).await?;
                    },
                    GameMessage::TellOtherPlayerMyLocation(p) => {
                        client.x = p.x;
                        client.y = p.y;
                        client.z = p.z;
                        client.area = rooms.with(room_id, |room| Box::pin(async move {
                            room.map_areas.get_area_map(p.map_area).ok()
                        })).await?;
                    },
                    GameMessage::PlayerWarpingToFloor(p) => {
                        client.area = rooms.with(room_id, |room| Box::pin(async move {
                            room.map_areas.get_area_map(p.area as u16).ok()
                        })).await?;
                    },
                    GameMessage::PlayerTeleported(p) => {
                        client.x = p.x;
                        client.y = p.y;
                        client.z = p.z;
                    },
                    GameMessage::PlayerStopped(p) => {
                        client.x = p.x;
                        client.y = p.y;
                        client.z = p.z;
                    },
                    GameMessage::PlayerLoadedIn(p) => {
                        client.x = p.x;
                        client.y = p.y;
                        client.z = p.z;
                    },
                    GameMessage::PlayerWalking(p) => {
                        client.x = p.x;
                        client.z = p.z;
                    },
                    GameMessage::PlayerRunning(p) => {
                        client.x = p.x;
                        client.z = p.z;
                    },
                    GameMessage::PlayerWarped(p) => {
                        client.x = p.x;
                        client.y = p.y;
                    },
                    // GameMessage::PlayerChangedFloor(p) => {client.area = MapArea::from_value(&room.mode.episode(), p.map).ok();},
                    GameMessage::InitializeSpeechNpc(p) => {
                        client.x = p.x;
                        client.z = p.z;
                    }
                    _ => {},
                }
                Ok::<_, ShipError>(())
            })}).await??;
    }
    Ok(client_location.get_client_neighbors(id).await?.into_iter()
       .map(move |client| {
           (client.client, SendShipPacket::Message(message.clone()))
       })
       .collect())
}

pub async fn charge_attack<EG>(id: ClientId,
                               charge: ChargeAttack,
                               entity_gateway: &mut EG,
                               client_location: &ClientLocation,
                               clients: &Clients,
                               item_state: &mut ItemState)
                               -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    let meseta = charge.meseta;
    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            // TODO: should probably validate this to be a legit number, I'd just hardcode 200 but vjaya
            take_meseta(&mut item_state, &mut entity_gateway, &client.character.id, Meseta(meseta)).await
        })}).await??;

    Ok(client_location.get_client_neighbors(id).await.unwrap().into_iter()
       .map(move |client| {
           (client.client, SendShipPacket::Message(Message::new(GameMessage::ChargeAttack(charge.clone()))))
       })
       .collect())
}

pub async fn player_uses_item<EG>(id: ClientId,
                                  player_use_tool: PlayerUseItem,
                                  entity_gateway: &mut EG,
                                  client_location: &ClientLocation,
                                  clients: &Clients,
                                  item_state: &mut ItemState)
                                  -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    let neighbors = client_location.get_all_clients_by_client(id).await?.into_iter();
    let area_client = client_location.get_local_client(id).await?;

    Ok(clients.with_mut(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            use_item(&mut item_state, &mut entity_gateway, &mut client.character, area_client, &ClientItemId(player_use_tool.item_id), 1).await
        })}).await??
       .into_iter()
       .flat_map(move |pkt| {
           let player_use_tool = player_use_tool.clone();
           neighbors.clone().map(move |client| {
               vec![(client.client, SendShipPacket::Message(Message::new(GameMessage::PlayerUseItem(player_use_tool.clone())))), (client.client, pkt.clone())]
           })
       })
       .flatten()
       .collect::<Vec<_>>()
    )
}

pub async fn player_used_medical_center<EG>(id: ClientId,
                                            pumc: PlayerUsedMedicalCenter,
                                            entity_gateway: &mut EG,
                                            client_location: &ClientLocation,
                                            clients: &Clients,
                                            item_state: &mut ItemState)
                                            -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            take_meseta(&mut item_state, &mut entity_gateway, &client.character.id, Meseta(10)).await
        })}).await??;

    let pumc = pumc.clone();
    Ok(client_location.get_client_neighbors(id).await.unwrap().into_iter()
       .map(move |client| {
           (client.client, SendShipPacket::Message(Message::new(GameMessage::PlayerUsedMedicalCenter(pumc.clone()))))
       })
       .collect())
}


pub async fn player_feed_mag<EG>(id: ClientId,
                                 mag_feed: PlayerFeedMag,
                                 entity_gateway: &mut EG,
                                 client_location: &ClientLocation,
                                 clients: &Clients,
                                 item_state: &mut ItemState)
                                 -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{

    let cmag_feed = mag_feed.clone();
    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            feed_mag(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(cmag_feed.mag_id), &ClientItemId(cmag_feed.item_id)).await
        })}).await??;

    Ok(client_location.get_client_neighbors(id).await.unwrap().into_iter()
       .map(move |client| {
           (client.client, SendShipPacket::Message(Message::new(GameMessage::PlayerFeedMag(mag_feed.clone()))))
       })
       .collect())
}

pub async fn player_equips_item<EG>(id: ClientId,
                                    pkt: PlayerEquipItem,
                                    entity_gateway: &mut EG,
                                    clients: &Clients,
                                    item_state: &mut ItemState)
                                    -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    let equip_slot = if pkt.sub_menu > 0 {
        ((pkt.sub_menu & 0x7) - 1) % 4
    }
    else {
        0
    };

    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            equip_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(pkt.item_id), equip_slot).await
        })}).await??;
    Ok(Vec::new()) // TODO: tell other players you equipped an item
}

pub async fn player_unequips_item<EG>(id: ClientId,
                                      pkt: PlayerUnequipItem,
                                      entity_gateway: &mut EG,
                                      clients: &Clients,
                                      item_state: &mut ItemState)
                                      -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            unequip_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(pkt.item_id)).await
        })}).await??;
    Ok(Vec::new()) // TODO: tell other players if you unequip an item
}

pub async fn player_sorts_items<EG>(id: ClientId,
                                    pkt: SortItems,
                                    entity_gateway: &mut EG,
                                    clients: &Clients,
                                    item_state: &mut ItemState)
                                    -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    let item_ids = pkt.item_ids
        .iter()
        .filter_map(|item_id| {
            if *item_id != 0 {
                Some(ClientItemId(*item_id))
            }
            else {
                None
            }
        })
        .collect();

    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            sort_inventory(&mut item_state, &mut entity_gateway, &client.character, item_ids).await
        })}).await??;
    Ok(Vec::new()) // TODO: clients probably care about each others item orders
}

pub async fn player_sells_item<EG> (id: ClientId,
                                    sold_item: PlayerSoldItem,
                                    entity_gateway: &mut EG,
                                    clients: &Clients,
                                    item_state: &mut ItemState)
                                    -> Result<Vec<(ClientId, SendShipPacket)>, ShipError>
where
    EG: EntityGateway + Clone + 'static,
{
    clients.with(id, |client| {
        let mut entity_gateway = entity_gateway.clone();
        let mut item_state = item_state.clone();
        Box::pin(async move {
            sell_item(&mut item_state, &mut entity_gateway, &client.character, ClientItemId(sold_item.item_id), sold_item.amount as u32).await
        })}).await??;
    Ok(Vec::new()) // TODO: send the packet to other clients
}