/*
    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 .
*/
use crate::{
    customer::DemandState,
    data::Gamedata,
    entity::{Entity, EntityT},
    interaction::{interact, tick_slot, InteractEffect, TickEffect},
};
use anyhow::{anyhow, bail, Result};
use hurrycurry_protocol::{
    glam::{IVec2, Vec2},
    ClientGamedata, ItemIndex, ItemLocation, Message, PacketC, PacketS, PlayerID, RecipeIndex,
    TileIndex,
};
use log::{info, warn};
use std::{
    collections::{HashMap, VecDeque},
    sync::Arc,
    time::{Duration, Instant},
};
#[derive(Debug, PartialEq)]
pub struct Involvement {
    pub recipe: RecipeIndex,
    pub progress: f32,
    pub working: usize,
}
#[derive(Debug, PartialEq)]
pub struct Item {
    pub kind: ItemIndex,
    pub active: Option,
}
pub struct Tile {
    pub kind: TileIndex,
    pub item: Option- ,
}
pub struct Player {
    pub name: String,
    pub character: i32,
    pub position: Vec2,
    pub last_position_ts: Instant,
    pub interacting: Option,
    pub item: Option- ,
    pub communicate_persist: Option,
}
pub struct Game {
    pub data: Arc,
    tiles: HashMap,
    pub players: HashMap,
    packet_out: VecDeque,
    demand: Option,
    pub points: i64,
    entities: Vec,
    end: Option,
}
impl Game {
    pub fn new() -> Self {
        Self {
            data: Gamedata::default().into(),
            packet_out: Default::default(),
            players: HashMap::new(),
            tiles: HashMap::new(),
            demand: None,
            end: None,
            entities: vec![],
            points: 0,
        }
    }
    fn unload(&mut self) {
        self.packet_out.push_back(PacketC::SetIngame {
            state: false,
            lobby: false,
        });
        for (id, _) in self.players.drain() {
            self.packet_out.push_back(PacketC::RemovePlayer { id })
        }
        for (pos, _) in self.tiles.drain() {
            self.packet_out.push_back(PacketC::UpdateMap {
                tile: pos,
                kind: None,
                neighbors: [None, None, None, None],
            })
        }
        self.demand = None;
    }
    pub fn load(&mut self, gamedata: Gamedata, timer: Option) {
        let players = self
            .players
            .iter()
            .filter(|(id, _)| id.0 >= 0)
            .map(|(id, p)| (*id, (p.name.to_owned(), p.character)))
            .collect::>();
        self.unload();
        self.data = gamedata.into();
        self.points = 0;
        self.end = timer.map(|dur| Instant::now() + dur);
        self.entities = self.data.entities.clone();
        for (&p, (tile, item)) in &self.data.initial_map {
            self.tiles.insert(
                p,
                Tile {
                    kind: *tile,
                    item: item.map(|i| Item {
                        kind: i,
                        active: None,
                    }),
                },
            );
        }
        for (id, (name, character)) in players {
            self.players.insert(
                id,
                Player {
                    item: None,
                    last_position_ts: Instant::now(),
                    character,
                    position: self.data.chef_spawn,
                    communicate_persist: None,
                    interacting: None,
                    name: name.clone(),
                },
            );
        }
        if !self.data.demands.is_empty() {
            self.demand = Some(DemandState::new(self.data.clone(), &self.tiles))
        }
        self.packet_out.extend(self.prime_client());
    }
    pub fn tiles(&self) -> &HashMap {
        &self.tiles
    }
    pub fn packet_out(&mut self) -> Option {
        self.packet_out.pop_front()
    }
    pub fn prime_client(&self) -> Vec {
        let mut out = Vec::new();
        out.push(PacketC::Data {
            data: ClientGamedata {
                item_names: self.data.item_names.clone(),
                tile_names: self.data.tile_names.clone(),
                tile_collide: self.data.tile_collide.clone(),
                tile_interact: self.data.tile_interact.clone(),
                map_names: self
                    .data
                    .map
                    .clone()
                    .keys()
                    .filter(|n| n.as_str() != "lobby")
                    .map(|s| s.to_owned())
                    .collect(),
                maps: self
                    .data
                    .map
                    .clone()
                    .into_iter()
                    .filter(|(n, _)| n != "lobby")
                    .collect(),
            },
        });
        for (&id, player) in &self.players {
            out.push(PacketC::AddPlayer {
                id,
                position: player.position,
                character: player.character,
                name: player.name.clone(),
            });
            if let Some(item) = &player.item {
                out.push(PacketC::SetItem {
                    location: ItemLocation::Player(id),
                    item: Some(item.kind),
                })
            }
            if let Some(c) = &player.communicate_persist {
                out.push(PacketC::Communicate {
                    player: id,
                    message: Some(c.to_owned()),
                    persist: true,
                })
            }
        }
        for (&tile, tdata) in &self.tiles {
            out.push(PacketC::UpdateMap {
                tile,
                neighbors: [
                    self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind),
                    self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind),
                    self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind),
                    self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind),
                ],
                kind: Some(tdata.kind.clone()),
            });
            if let Some(item) = &tdata.item {
                out.push(PacketC::SetItem {
                    location: ItemLocation::Tile(tile),
                    item: Some(item.kind),
                })
            }
        }
        out.push(self.score());
        out.push(PacketC::SetIngame {
            state: true,
            lobby: self.demand.is_none(),
        });
        out
    }
    pub fn score(&self) -> PacketC {
        PacketC::Score {
            time_remaining: self.end.map(|t| (t - Instant::now()).as_secs_f32()),
            points: self.points,
            demands_failed: self.demand.as_ref().map(|d| d.failed).unwrap_or_default(),
            demands_completed: self
                .demand
                .as_ref()
                .map(|d| d.completed)
                .unwrap_or_default(),
        }
    }
    pub fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<()> {
        let points_before = self.points;
        match packet {
            PacketS::Join { name, character } => {
                if self.players.contains_key(&player) {
                    bail!("You already joined.")
                }
                let position = if player.0 < 0 {
                    self.data.customer_spawn
                } else {
                    self.data.chef_spawn
                };
                self.players.insert(
                    player,
                    Player {
                        item: None,
                        last_position_ts: Instant::now(),
                        character,
                        position,
                        communicate_persist: None,
                        interacting: None,
                        name: name.clone(),
                    },
                );
                self.packet_out.push_back(PacketC::AddPlayer {
                    id: player,
                    name,
                    position,
                    character,
                });
            }
            PacketS::Leave => {
                let p = self
                    .players
                    .remove(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                if let Some(item) = p.item {
                    let pos = p.position.floor().as_ivec2();
                    if let Some(tile) = self.tiles.get_mut(&pos) {
                        if tile.item.is_none() {
                            self.packet_out.push_back(PacketC::SetItem {
                                location: ItemLocation::Tile(pos),
                                item: Some(item.kind),
                            });
                            tile.item = Some(item);
                        }
                    }
                }
                self.packet_out
                    .push_back(PacketC::RemovePlayer { id: player })
            }
            PacketS::Position { pos, rot, boosting } => {
                let pid = player;
                let player = self
                    .players
                    .get_mut(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                // let dt = player.last_position_ts.elapsed().as_secs_f32();
                // let dist = pos.distance(player.position);
                // let speed = dist / dt;
                // let interact_dist = player
                //     .interacting
                //     .map(|p| (p.as_vec2() + Vec2::splat(0.5)).distance(player.position))
                //     .unwrap_or_default();
                // let movement_ok = speed < PLAYER_SPEED_LIMIT && dist < 1. && interact_dist < 2.;
                // if movement_ok {
                player.position = pos;
                player.last_position_ts = Instant::now();
                // }
                self.packet_out.push_back(PacketC::Position {
                    player: pid,
                    pos: player.position,
                    rot,
                    boosting,
                });
                // if !movement_ok {
                //     bail!(
                //         "{:?} moved to quickly. speed={speed:.02} dist={dist:.02}",
                //         player.name
                //     )
                // }
            }
            PacketS::Collide { player, force } => {
                self.packet_out
                    .push_back(PacketC::Collide { player, force });
            }
            PacketS::Interact { pos } => {
                let pid = player;
                let player = self
                    .players
                    .get_mut(&pid)
                    .ok_or(anyhow!("player does not exist"))?;
                let (pos, edge) = match (pos, player.interacting) {
                    (None, None) => return Ok(()), // this is silent because of auto release
                    (None, Some(pos)) => (pos, false),
                    (Some(pos), None) => (pos, true),
                    (Some(_), Some(_)) => bail!("already interacting"),
                };
                let entpos = pos.as_vec2() + Vec2::splat(0.5);
                if edge && entpos.distance(player.position) > 2. {
                    bail!("interacting too far from player");
                }
                let tile = self
                    .tiles
                    .get_mut(&pos)
                    .ok_or(anyhow!("tile does not exist"))?;
                // No going back from here on
                player.interacting = if edge { Some(pos) } else { None };
                let other_pid = if !self.data.is_tile_interactable(tile.kind) {
                    self.players
                        .iter()
                        .find(|(id, p)| **id != pid && p.position.distance(entpos) < 0.7)
                        .map(|(&id, _)| id)
                } else {
                    None
                };
                if let Some(base_pid) = other_pid {
                    let [other, this] = self
                        .players
                        .get_many_mut([&pid, &base_pid])
                        .ok_or(anyhow!("interacting with yourself. this is impossible"))?;
                    if this.character < 0 || other.character < 0 {
                        bail!("You shall not interact with customers.")
                    }
                    interact_effect(
                        &self.data,
                        edge,
                        &mut this.item,
                        ItemLocation::Player(base_pid),
                        &mut other.item,
                        ItemLocation::Player(pid),
                        None,
                        &mut self.packet_out,
                        &mut self.points,
                        false,
                    )
                } else {
                    let player = self
                        .players
                        .get_mut(&pid)
                        .ok_or(anyhow!("player does not exist"))?;
                    interact_effect(
                        &self.data,
                        edge,
                        &mut tile.item,
                        ItemLocation::Tile(pos),
                        &mut player.item,
                        ItemLocation::Player(pid),
                        Some(tile.kind),
                        &mut self.packet_out,
                        &mut self.points,
                        false,
                    )
                }
            }
            PacketS::Communicate { message, persist } => {
                info!("{player:?} message {message:?}");
                if persist {
                    if let Some(player) = self.players.get_mut(&player) {
                        player.communicate_persist = message.clone()
                    }
                }
                self.packet_out.push_back(PacketC::Communicate {
                    player,
                    message,
                    persist,
                })
            }
            PacketS::ReplaceHand { item } => {
                let pdata = self
                    .players
                    .get_mut(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                pdata.item = item.map(|i| Item {
                    kind: i,
                    active: None,
                });
                self.packet_out.push_back(PacketC::SetItem {
                    location: ItemLocation::Player(player),
                    item,
                })
            }
            PacketS::ReplayTick { .. } => bail!("packet not supported in this session"),
        }
        if self.points != points_before {
            self.packet_out.push_back(self.score())
        }
        Ok(())
    }
    /// Returns true if the game should end
    pub fn tick(&mut self, dt: f32) -> bool {
        if let Some(demand) = &mut self.demand {
            let mut packet_out = Vec::new();
            if let Err(err) = demand.tick(
                &mut packet_out,
                &mut self.tiles,
                &self.data,
                dt,
                &mut self.points,
            ) {
                warn!("demand tick {err}");
            }
            if demand.score_changed {
                demand.score_changed = false;
                self.packet_out.push_back(self.score());
            }
            for (player, packet) in packet_out {
                if let Err(err) = self.packet_in(player, packet) {
                    warn!("demand packet {err}");
                }
            }
        }
        for (&pos, tile) in &mut self.tiles {
            if let Some(effect) = tick_slot(dt, &self.data, Some(tile.kind), &mut tile.item) {
                match effect {
                    TickEffect::Progress(warn) => self.packet_out.push_back(PacketC::SetProgress {
                        warn,
                        item: ItemLocation::Tile(pos),
                        progress: tile
                            .item
                            .as_ref()
                            .unwrap()
                            .active
                            .as_ref()
                            .map(|i| i.progress),
                    }),
                    TickEffect::Produce => {
                        self.packet_out.push_back(PacketC::SetItem {
                            location: ItemLocation::Tile(pos),
                            item: tile.item.as_ref().map(|i| i.kind),
                        });
                    }
                }
            }
        }
        for (&pid, player) in &mut self.players {
            if let Some(effect) = tick_slot(dt, &self.data, None, &mut player.item) {
                match effect {
                    TickEffect::Progress(warn) => self.packet_out.push_back(PacketC::SetProgress {
                        warn,
                        item: ItemLocation::Player(pid),
                        progress: player
                            .item
                            .as_ref()
                            .unwrap()
                            .active
                            .as_ref()
                            .map(|i| i.progress),
                    }),
                    TickEffect::Produce => {
                        self.packet_out.push_back(PacketC::SetItem {
                            location: ItemLocation::Player(pid),
                            item: player.item.as_ref().map(|i| i.kind),
                        });
                    }
                }
            }
        }
        let mut players_auto_release = Vec::new();
        for (pid, player) in &mut self.players {
            if let Some(pos) = player.interacting {
                if let Some(tile) = self.tiles.get(&pos) {
                    if let Some(item) = &tile.item {
                        if let Some(involvement) = &item.active {
                            if involvement.progress >= 1. {
                                players_auto_release.push(*pid);
                            }
                        }
                    }
                }
            }
        }
        for pid in players_auto_release.drain(..) {
            let _ = self.packet_in(pid, PacketS::Interact { pos: None });
        }
        for entity in &mut self.entities {
            if let Err(e) = entity.tick(
                &self.data,
                &mut self.points,
                &mut self.packet_out,
                &mut self.tiles,
                dt,
            ) {
                warn!("entity tick failed: {e}")
            }
        }
        return self.end.map(|t| t < Instant::now()).unwrap_or_default();
    }
    pub fn count_chefs(&self) -> usize {
        self.players
            .iter()
            .map(|(_, p)| if p.character >= 0 { 1 } else { 0 })
            .sum()
    }
}
impl From for Tile {
    fn from(kind: TileIndex) -> Self {
        Self { kind, item: None }
    }
}
pub fn interact_effect(
    data: &Gamedata,
    edge: bool,
    this: &mut Option- ,
    this_loc: ItemLocation,
    other: &mut Option- ,
    other_loc: ItemLocation,
    this_tile_kind: Option,
    packet_out: &mut VecDeque,
    points: &mut i64,
    automated: bool,
) {
    let this_had_item = this.is_some();
    let other_had_item = other.is_some();
    if let Some(effect) = interact(&data, edge, this_tile_kind, this, other, points, automated) {
        match effect {
            InteractEffect::Put => {
                info!("put {this_loc} <- {other_loc}");
                packet_out.push_back(PacketC::MoveItem {
                    from: other_loc,
                    to: this_loc,
                })
            }
            InteractEffect::Take => {
                info!("take {this_loc} -> {other_loc}");
                packet_out.push_back(PacketC::MoveItem {
                    from: this_loc,
                    to: other_loc,
                })
            }
            InteractEffect::Produce => {
                info!("produce {this_loc} <~ {other_loc}");
                if this_had_item {
                    packet_out.push_back(PacketC::SetProgress {
                        item: this_loc,
                        progress: None,
                        warn: false,
                    });
                    packet_out.push_back(PacketC::SetItem {
                        location: this_loc,
                        item: None,
                    });
                }
                if other_had_item {
                    packet_out.push_back(PacketC::MoveItem {
                        from: other_loc,
                        to: this_loc,
                    });
                    packet_out.push_back(PacketC::SetItem {
                        location: this_loc,
                        item: None,
                    });
                }
                if let Some(i) = &other {
                    packet_out.push_back(PacketC::SetItem {
                        location: this_loc,
                        item: Some(i.kind),
                    });
                    packet_out.push_back(PacketC::MoveItem {
                        from: this_loc,
                        to: other_loc,
                    })
                }
                if let Some(i) = &this {
                    packet_out.push_back(PacketC::SetItem {
                        location: this_loc,
                        item: Some(i.kind),
                    });
                }
            }
        }
    }
}