/*
    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::DemandState,
    data::Gamedata,
    interaction::{interact, tick_slot, InteractEffect, TickEffect},
    protocol::{
        ItemIndex, ItemLocation, Message, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex,
    },
};
use anyhow::{anyhow, bail, Result};
use glam::{IVec2, Vec2};
use log::{info, warn};
use std::{
    collections::{HashMap, VecDeque},
    ops::Deref,
    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 {
    data: Arc,
    tiles: HashMap,
    pub players: HashMap,
    packet_out: VecDeque,
    demand: Option,
    pub points: i64,
    end: Option,
}
impl Game {
    pub fn new() -> Self {
        Self {
            data: Gamedata::default().into(),
            packet_out: Default::default(),
            players: Default::default(),
            tiles: Default::default(),
            demand: None,
            end: None,
            points: 0,
        }
    }
    fn unload(&mut self) {
        self.packet_out
            .push_back(PacketC::SetIngame { state: 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);
        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: 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::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 });
        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 } => {
                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 } => {
                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(&pid)
                    .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")
                }
                let entpos = pos.as_vec2() + Vec2::splat(0.5);
                if edge && entpos.distance(player.position) > 2. {
                    bail!("interacting too far from player");
                }
                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"))?;
                    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,
                    )
                } 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,
                    )
                }
            }
            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,
                })
            }
        }
        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),
                        });
                    }
                }
            }
        }
        return self.end.map(|t| t < Instant::now()).unwrap_or_default();
    }
}
impl From for Tile {
    fn from(kind: TileIndex) -> Self {
        Self { kind, item: None }
    }
}
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,
) {
    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) {
        match effect {
            InteractEffect::Put => packet_out.push_back(PacketC::MoveItem {
                from: other_loc,
                to: this_loc,
            }),
            InteractEffect::Take => packet_out.push_back(PacketC::MoveItem {
                from: this_loc,
                to: other_loc,
            }),
            InteractEffect::Produce => {
                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),
                    });
                }
            }
        }
    }
}