/*
    Hurry Curry! - 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::Tile};
use anyhow::{anyhow, Result};
use fake::{faker, Fake};
use hurrycurry_protocol::{glam::IVec2, DemandIndex, Message, PacketS, PlayerID};
use log::info;
use movement::MovementBase;
use pathfinding::{find_path, Path};
use rand::{random, thread_rng};
use std::{
    collections::{HashMap, HashSet},
    sync::Arc,
};
pub struct DemandState {
    data: Arc,
    walkable: HashSet,
    chairs: HashMap,
    customer_id_counter: PlayerID,
    customers: HashMap,
    spawn_cooldown: f32,
    pub completed: usize,
    pub failed: usize,
    pub score_changed: bool,
}
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,
    },
}
pub struct Customer {
    movement: MovementBase,
    state: CustomerState,
}
impl DemandState {
    pub fn new(data: Arc, map: &HashMap) -> Self {
        let chair = data.get_tile_by_name("chair");
        Self {
            score_changed: true,
            completed: 0,
            failed: 0,
            walkable: map
                .iter()
                .filter(|(_, v)| !data.is_tile_colliding(v.kind))
                .map(|(e, _)| *e)
                .collect(),
            chairs: map
                .iter()
                .filter(|(_, v)| Some(v.kind) == chair)
                .map(|(e, _)| (*e, true))
                .collect(),
            customer_id_counter: PlayerID(0),
            customers: Default::default(),
            data,
            spawn_cooldown: 0.,
        }
    }
}
impl DemandState {
    pub fn tick(
        &mut self,
        packets_out: &mut Vec<(PlayerID, PacketS)>,
        tiles: &mut HashMap,
        data: &Gamedata,
        dt: f32,
        points: &mut i64,
    ) -> Result<()> {
        self.spawn_cooldown -= dt;
        self.spawn_cooldown = self.spawn_cooldown.max(0.);
        if self.customers.len() < 5 && self.spawn_cooldown <= 0. {
            self.spawn_cooldown = 10. + random::() * 10.;
            self.customer_id_counter.0 -= 1;
            let id = self.customer_id_counter;
            packets_out.push((
                id,
                PacketS::Join {
                    name: faker::name::fr_fr::Name().fake(),
                    character: -1 - (random::() as i32),
                },
            ));
            let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?;
            let from = data.customer_spawn.as_ivec2();
            let path = find_path(&self.walkable, from, chair)
                .ok_or(anyhow!("no path from {from} to {chair}"))?;
            info!("{id:?} -> entering");
            self.customers.insert(
                id,
                Customer {
                    movement: MovementBase::new(data.customer_spawn),
                    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 } => {
                    packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
                    if path.is_done() {
                        let demand = DemandIndex(random::() % self.data.demands.len());
                        packets_out.push((
                            id,
                            PacketS::Communicate {
                                message: Some(Message::Item(data.demand(demand).from)),
                                persist: true,
                            },
                        ));
                        info!("{id:?} -> waiting");
                        p.state = CustomerState::Waiting {
                            chair: *chair,
                            timeout: 90. + random::() * 60.,
                            demand,
                        };
                    }
                }
                CustomerState::Waiting {
                    chair,
                    demand,
                    timeout,
                } => {
                    *timeout -= dt;
                    if *timeout <= 0. {
                        packets_out.push((
                            id,
                            PacketS::Communicate {
                                message: None,
                                persist: true,
                            },
                        ));
                        packets_out.push((
                            id,
                            PacketS::Communicate {
                                message: Some(Message::Effect("angry".to_string())),
                                persist: false,
                            },
                        ));
                        let path = find_path(
                            &self.walkable,
                            p.movement.position.as_ivec2(),
                            data.customer_spawn.as_ivec2(),
                        )
                        .expect("no path to exit");
                        *self.chairs.get_mut(&chair).unwrap() = true;
                        self.failed += 1;
                        *points -= 1;
                        self.score_changed = true;
                        info!("{id:?} -> exiting");
                        p.state = CustomerState::Exiting { path }
                    } else {
                        let demand_data = &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 tiles
                                    .get(&pos)
                                    .map(|t| {
                                        t.item
                                            .as_ref()
                                            .map(|i| i.kind == demand_data.from)
                                            .unwrap_or_default()
                                    })
                                    .unwrap_or_default()
                                {
                                    Some(pos)
                                } else {
                                    None
                                }
                            });
                        if let Some(pos) = demand_pos {
                            packets_out.push((
                                id,
                                PacketS::Communicate {
                                    persist: true,
                                    message: None,
                                },
                            ));
                            packets_out.push((
                                id,
                                PacketS::Communicate {
                                    message: Some(Message::Effect("satisfied".to_string())),
                                    persist: false,
                                },
                            ));
                            packets_out.push((id, PacketS::Interact { pos: Some(pos) }));
                            packets_out.push((id, PacketS::Interact { pos: None }));
                            info!("{id:?} -> eating");
                            p.state = CustomerState::Eating {
                                demand: *demand,
                                target: pos,
                                progress: 0.,
                                chair: *chair,
                            }
                        }
                    }
                }
                CustomerState::Eating {
                    demand,
                    target,
                    progress,
                    chair,
                } => {
                    let demand = data.demand(*demand);
                    *progress += dt / demand.duration;
                    if *progress >= 1. {
                        packets_out.push((id, PacketS::ReplaceHand { item: demand.to }));
                        if demand.to.is_some() {
                            packets_out.push((id, PacketS::Interact { pos: Some(*target) }));
                            packets_out.push((id, PacketS::Interact { pos: None }));
                        }
                        let path = find_path(
                            &self.walkable,
                            p.movement.position.as_ivec2(),
                            data.customer_spawn.as_ivec2(),
                        )
                        .ok_or(anyhow!("no path to exit"))?;
                        *self.chairs.get_mut(&chair).unwrap() = true;
                        self.completed += 1;
                        *points += demand.points;
                        self.score_changed = true;
                        info!("{id:?} -> exiting");
                        p.state = CustomerState::Exiting { path }
                    }
                }
                CustomerState::Exiting { path } => {
                    packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
                    if path.is_done() {
                        info!("{id:?} -> leave");
                        packets_out.push((id, PacketS::Leave));
                        customers_to_remove.push(id);
                    }
                }
            }
        }
        for c in customers_to_remove {
            self.customers.remove(&c).unwrap();
        }
        Ok(())
    }
    fn select_chair(&mut self) -> Option {
        use rand::seq::IteratorRandom;
        let (chosen, free) = self
            .chairs
            .iter_mut()
            .filter(|(_p, free)| **free)
            .choose(&mut thread_rng())?;
        *free = false;
        Some(*chosen)
    }
}