/*
    Undercooked - a game about cooking
    Copyright 2024 metamuffin
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
*/
pub mod movement;
mod pathfinding;
use crate::{
    data::Gamedata,
    game::Game,
    protocol::{DemandIndex, ItemIndex, Message, PacketC, PacketS, PlayerID},
    state::State,
};
use glam::{IVec2, Vec2};
use log::{debug, error};
use movement::MovementBase;
use pathfinding::{find_path, Path};
use rand::{random, thread_rng};
use std::{
    collections::{HashMap, HashSet},
    sync::Arc,
    time::Duration,
};
use tokio::{
    sync::{broadcast, RwLock},
    time::interval,
};
struct CustomerManager {
    disabled: bool,
    walkable: HashSet,
    chairs: HashMap,
    items: HashMap,
    customers: HashMap,
    customer_id_counter: PlayerID,
    demand: DemandState,
}
struct DemandState {
    data: Gamedata,
}
enum CustomerState {
    Entering {
        path: Path,
        chair: IVec2,
    },
    Waiting {
        demand: DemandIndex,
        chair: IVec2,
        timeout: f32,
    },
    Eating {
        demand: DemandIndex,
        target: IVec2,
        progress: f32,
        chair: IVec2,
    },
    Exiting {
        path: Path,
    },
}
struct Customer {
    movement: MovementBase,
    state: CustomerState,
}
pub async fn customer(gstate: Arc>, mut grx: broadcast::Receiver) {
    let mut state = CustomerManager {
        customer_id_counter: PlayerID(0),
        walkable: Default::default(),
        chairs: Default::default(),
        items: Default::default(),
        customers: Default::default(),
        disabled: true,
        demand: DemandState {
            data: Gamedata::default(),
        },
    };
    let initial = gstate.write().await.game.prime_client();
    for packet in initial {
        state.packet(packet);
    }
    let mut interval = interval(Duration::from_millis(40));
    let mut packets_out = Vec::new();
    loop {
        tokio::select! {
            packet = grx.recv() => {
                let packet = packet.unwrap();
                match packet {
                    PacketC::PutItem { .. }
                    | PacketC::TakeItem { .. }
                    | PacketC::SetTileItem { .. } => {
                        let g = gstate.read().await;
                        update_items(&mut state, &g.game)
                    },
                    _ => ()
                }
                state.packet(packet);
            }
            _ = interval.tick() => {
                if !state.disabled {
                    state.tick(&mut packets_out, 0.04);
                    for (player,packet) in packets_out.drain(..) {
                        if let Err(e) = gstate.write().await.packet_in(player, packet).await {
                            error!("customer misbehaved: {e}")
                        }
                    }
                }
            }
        }
    }
}
// TODO very inefficient, please do that incrementally
fn update_items(state: &mut CustomerManager, game: &Game) {
    state.items.clear();
    for (&pos, tile) in game.tiles() {
        if let Some(item) = &tile.item {
            state.items.insert(pos, item.kind);
        }
    }
}
impl DemandState {
    pub fn target_customer_count(&self) -> usize {
        // TODO insert sofa magic formula
        5
    }
    pub fn generate_demand(&self) -> DemandIndex {
        // TODO insert sofa magic formula
        DemandIndex(random::() % self.data.demands.len())
    }
}
impl CustomerManager {
    pub fn packet(&mut self, packet: PacketC) {
        match packet {
            PacketC::Data { data } => {
                self.disabled = data.demands.is_empty();
                self.demand.data = data;
            }
            PacketC::RemovePlayer { id } => {
                self.customers.remove(&id);
            }
            PacketC::UpdateMap {
                tile: pos,
                kind: Some(tile),
                ..
            } => {
                let tilename = self.demand.data.tile_name(tile);
                if !self.demand.data.is_tile_colliding(tile) {
                    self.walkable.insert(pos);
                }
                if tilename == "chair" {
                    self.chairs.insert(pos, true);
                }
            }
            _ => (),
        }
    }
    pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) {
        if self.customers.len() < self.demand.target_customer_count() {
            self.customer_id_counter.0 -= 1;
            let id = self.customer_id_counter;
            packets_out.push((
                id,
                PacketS::Join {
                    name: "George".to_string(),
                    character: -2,
                },
            ));
            let chair = select_chair(&mut self.chairs);
            let path = find_path(
                &self.walkable,
                self.demand.data.customer_spawn.as_ivec2(),
                chair,
            )
            .expect("no path");
            self.customers.insert(
                id,
                Customer {
                    movement: MovementBase {
                        position: self.demand.data.customer_spawn,
                        facing: Vec2::X,
                        vel: Vec2::ZERO,
                    },
                    state: CustomerState::Entering { path, chair },
                },
            );
        }
        let mut customers_to_remove = Vec::new();
        for (&id, p) in &mut self.customers {
            match &mut p.state {
                CustomerState::Entering { path, chair } => {
                    debug!("{id:?} entering");
                    packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
                    if path.is_done() {
                        let demand = self.demand.generate_demand();
                        packets_out.push((
                            id,
                            PacketS::Communicate {
                                message: Some(Message::Item(self.demand.data.demand(demand).from)),
                            },
                        ));
                        p.state = CustomerState::Waiting {
                            chair: *chair,
                            timeout: 60.,
                            demand,
                        };
                    }
                }
                CustomerState::Waiting {
                    chair,
                    demand,
                    timeout,
                } => {
                    debug!("{id:?} waiting");
                    *timeout -= dt;
                    if *timeout <= 0. {
                        packets_out.push((id, PacketS::Communicate { message: None }));
                        let path = find_path(
                            &self.walkable,
                            p.movement.position.as_ivec2(),
                            self.demand.data.customer_spawn.as_ivec2(),
                        )
                        .expect("no path to exit");
                        *self.chairs.get_mut(&chair).unwrap() = true;
                        p.state = CustomerState::Exiting { path }
                    } else {
                        let demand_data = &self.demand.data.demand(*demand);
                        let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y]
                            .into_iter()
                            .find_map(|off| {
                                let pos = *chair + off;
                                if self.items.get(&pos) == Some(&demand_data.from) {
                                    Some(pos)
                                } else {
                                    None
                                }
                            });
                        if let Some(pos) = demand_pos {
                            packets_out.push((id, PacketS::Communicate { message: None }));
                            for edge in [true, false] {
                                packets_out.push((id, PacketS::Interact { pos, edge }))
                            }
                            p.state = CustomerState::Eating {
                                demand: *demand,
                                target: pos,
                                progress: 0.,
                                chair: *chair,
                            }
                        }
                    }
                }
                CustomerState::Eating {
                    demand,
                    target,
                    progress,
                    chair,
                } => {
                    debug!("{id:?} eating");
                    let demand = self.demand.data.demand(*demand);
                    *progress += dt / demand.duration;
                    if *progress >= 1. {
                        packets_out.push((
                            id,
                            PacketS::ReplaceHand {
                                item: Some(demand.to),
                            },
                        ));
                        for edge in [true, false] {
                            packets_out.push((id, PacketS::Interact { pos: *target, edge }))
                        }
                        let path = find_path(
                            &self.walkable,
                            p.movement.position.as_ivec2(),
                            self.demand.data.customer_spawn.as_ivec2(),
                        )
                        .expect("no path to exit");
                        *self.chairs.get_mut(&chair).unwrap() = true;
                        p.state = CustomerState::Exiting { path }
                    }
                }
                CustomerState::Exiting { path } => {
                    debug!("{id:?} exiting");
                    packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
                    if path.is_done() {
                        packets_out.push((id, PacketS::Leave));
                        customers_to_remove.push(id);
                    }
                }
            }
        }
        for c in customers_to_remove {
            self.customers.remove(&c).unwrap();
        }
    }
}
pub fn select_chair(chairs: &mut HashMap) -> IVec2 {
    use rand::seq::IteratorRandom;
    let (chosen, free) = chairs
        .iter_mut()
        .filter(|(_p, free)| **free)
        .choose(&mut thread_rng())
        .unwrap();
    *free = false;
    *chosen
}