/*
    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 .
    
*/
use crate::{
    customer::movement::PLAYER_SPEED_LIMIT,
    data::Gamedata,
    interaction::{interact, tick_tile, InteractEffect, TickEffect},
    protocol::{ItemIndex, Message, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex},
};
use anyhow::{anyhow, bail, Result};
use glam::{IVec2, Vec2};
use log::info;
use std::{
    collections::{HashMap, VecDeque},
    ops::Deref,
    sync::Arc,
    time::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: Option,
}
pub struct Game {
    data: Arc,
    tiles: HashMap,
    players: HashMap,
    packet_out: VecDeque,
}
impl Game {
    pub fn new(gamedata: Arc) -> Self {
        let mut g = Self {
            data: gamedata.clone(),
            packet_out: Default::default(),
            players: Default::default(),
            tiles: Default::default(),
        };
        for (&p, (tile, item)) in &gamedata.initial_map {
            g.tiles.insert(
                p,
                Tile {
                    kind: *tile,
                    item: item.map(|i| Item {
                        kind: i,
                        active: None,
                    }),
                },
            );
        }
        g
    }
    pub fn tiles(&self) -> &HashMap {
        &self.tiles
    }
    pub fn packet_out(&mut self) -> Option {
        self.packet_out.pop_front()
    }
    pub fn prime_client(&self, id: PlayerID) -> Vec {
        let mut out = Vec::new();
        out.push(PacketC::Init {
            id,
            data: self.data.deref().to_owned(),
        });
        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::SetPlayerItem {
                    player: id,
                    item: Some(item.kind),
                })
            }
            if let Some(c) = &player.communicate {
                out.push(PacketC::Communicate {
                    player: id,
                    message: Some(c.to_owned()),
                })
            }
        }
        for (&tile, tdata) in &self.tiles {
            out.push(PacketC::UpdateMap {
                pos: 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),
                ],
                tile: tdata.kind.clone(),
            });
            if let Some(item) = &tdata.item {
                out.push(PacketC::SetTileItem {
                    tile,
                    item: Some(item.kind),
                })
            }
        }
        out
    }
    pub fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<()> {
        match packet {
            PacketS::Join { name, character } => {
                self.players.insert(
                    player,
                    Player {
                        item: None,
                        last_position_ts: Instant::now(),
                        character,
                        position: if player.0 < 0 {
                            self.data.customer_spawn
                        } else {
                            self.data.chef_spawn
                        },
                        communicate: None,
                        interacting: None,
                        name: name.clone(),
                    },
                );
                self.packet_out.push_back(PacketC::AddPlayer {
                    id: player,
                    name,
                    position: self.data.chef_spawn,
                    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::SetTileItem {
                                tile: pos,
                                item: Some(item.kind),
                            });
                            tile.item = Some(item);
                        }
                    }
                }
                self.packet_out
                    .push_back(PacketC::RemovePlayer { id: player })
            }
            PacketS::Position { pos, rot } => {
                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,
                });
                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, edge } => {
                info!("interact {pos:?} edge={edge}");
                let pid = player;
                let player = self
                    .players
                    .get_mut(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                let tile = self
                    .tiles
                    .get_mut(&pos)
                    .ok_or(anyhow!("tile does not exist"))?;
                if edge && player.interacting.is_some() {
                    bail!("already interacting")
                }
                if !edge && player.interacting != Some(pos) {
                    bail!("already not interacting here")
                }
                if edge && (pos.as_vec2() + Vec2::splat(0.5)).distance(player.position) > 2. {
                    bail!("interacting too far from player");
                }
                let tile_had_item = tile.item.is_some();
                let player_had_item = player.item.is_some();
                if let Some(effect) = interact(&self.data, edge, tile, player) {
                    match effect {
                        InteractEffect::Put => self.packet_out.push_back(PacketC::PutItem {
                            player: pid,
                            tile: pos,
                        }),
                        InteractEffect::Take => self.packet_out.push_back(PacketC::TakeItem {
                            player: pid,
                            tile: pos,
                        }),
                        InteractEffect::Produce => {
                            if tile_had_item {
                                self.packet_out.push_back(PacketC::SetActive {
                                    tile: pos,
                                    progress: None,
                                    warn: false,
                                });
                                self.packet_out.push_back(PacketC::SetTileItem {
                                    tile: pos,
                                    item: None,
                                });
                            }
                            if player_had_item {
                                self.packet_out.push_back(PacketC::PutItem {
                                    player: pid,
                                    tile: pos,
                                });
                                self.packet_out.push_back(PacketC::SetTileItem {
                                    tile: pos,
                                    item: None,
                                });
                            }
                            if let Some(i) = &player.item {
                                self.packet_out.push_back(PacketC::SetTileItem {
                                    tile: pos,
                                    item: Some(i.kind),
                                });
                                self.packet_out.push_back(PacketC::TakeItem {
                                    player: pid,
                                    tile: pos,
                                })
                            }
                            if let Some(i) = &tile.item {
                                self.packet_out.push_back(PacketC::SetTileItem {
                                    tile: pos,
                                    item: Some(i.kind),
                                });
                            }
                        }
                    }
                }
                player.interacting = if edge { Some(pos) } else { None };
            }
            PacketS::Communicate { message } => {
                info!("{player:?} message {message:?}");
                if let Some(player) = self.players.get_mut(&player) {
                    player.communicate = message.clone()
                }
                self.packet_out
                    .push_back(PacketC::Communicate { player, message })
            }
            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::SetPlayerItem { player, item })
            }
        }
        Ok(())
    }
    pub fn tick(&mut self, dt: f32) {
        for (&pos, tile) in &mut self.tiles {
            if let Some(effect) = tick_tile(dt, &self.data, tile) {
                match effect {
                    TickEffect::Progress(warn) => self.packet_out.push_back(PacketC::SetActive {
                        warn,
                        tile: pos,
                        progress: tile
                            .item
                            .as_ref()
                            .unwrap()
                            .active
                            .as_ref()
                            .map(|i| i.progress),
                    }),
                    TickEffect::Produce => {
                        self.packet_out.push_back(PacketC::SetTileItem {
                            tile: pos,
                            item: tile.item.as_ref().map(|i| i.kind),
                        });
                    }
                }
            }
        }
    }
}
impl From for Tile {
    fn from(kind: TileIndex) -> Self {
        Self { kind, item: None }
    }
}