/*
    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::{
    data::Gamedata,
    entity::{Entity, EntityT},
    interaction::{interact, tick_slot, InteractEffect, TickEffect},
    spatial_index::SpatialIndex,
};
use anyhow::{anyhow, bail, Result};
use hurrycurry_protocol::{
    glam::{IVec2, Vec2},
    movement::MovementBase,
    ClientGamedata, ItemIndex, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID,
    RecipeIndex, Score, TileIndex,
};
use log::{info, warn};
use std::{
    collections::{HashMap, HashSet, VecDeque},
    sync::{Arc, RwLock},
    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 interacting: Option,
    pub item: Option- ,
    pub communicate_persist: Option,
    pub movement: MovementBase,
    pub direction: Vec2,
    pub boost: bool,
    pub last_position_update: Instant,
}
pub struct Game {
    pub data: Arc,
    pub tiles: HashMap,
    pub walkable: HashSet,
    pub players: HashMap,
    pub players_spatial_index: SpatialIndex,
    entities: Arc>>,
    end: Option,
    pub lobby: bool,
    pub environment_effects: HashSet,
    pub score_changed: bool,
    pub score: Score,
    pub player_id_counter: PlayerID,
}
impl Default for Game {
    fn default() -> Self {
        Self::new()
    }
}
impl Game {
    pub fn new() -> Self {
        Self {
            lobby: false,
            data: Gamedata::default().into(),
            players: HashMap::new(),
            tiles: HashMap::new(),
            walkable: HashSet::new(),
            end: None,
            entities: Arc::new(RwLock::new(vec![])),
            players_spatial_index: SpatialIndex::default(),
            score: Score::default(),
            environment_effects: HashSet::default(),
            score_changed: false,
            player_id_counter: PlayerID(1),
        }
    }
    fn unload(&mut self, packet_out: &mut VecDeque) {
        packet_out.push_back(PacketC::SetIngame {
            state: false,
            lobby: false,
        });
        for (id, _) in self.players.drain() {
            packet_out.push_back(PacketC::RemovePlayer { id })
        }
        for (pos, _) in self.tiles.drain() {
            packet_out.push_back(PacketC::UpdateMap {
                tile: pos,
                kind: None,
                neighbors: [None, None, None, None],
            })
        }
        self.score = Score::default();
        self.end = None;
        self.environment_effects.clear();
        self.walkable.clear();
    }
    pub fn load(
        &mut self,
        gamedata: Gamedata,
        timer: Option,
        packet_out: &mut VecDeque,
    ) {
        let players = self
            .players
            .iter()
            .filter(|(_, p)| p.character >= 0)
            .map(|(id, p)| (*id, (p.name.to_owned(), p.character)))
            .collect::>();
        self.unload(packet_out);
        self.lobby = gamedata.map_name == "lobby";
        self.data = gamedata.into();
        self.score = Score {
            time_remaining: timer.map(|dur| dur.as_secs_f64()).unwrap_or(0.),
            ..Default::default()
        };
        self.end = timer.map(|dur| Instant::now() + dur);
        self.entities = Arc::new(RwLock::new(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,
                    }),
                },
            );
            if !self.data.tile_collide[tile.0] {
                self.walkable.insert(p);
            }
        }
        for (id, (name, character)) in players {
            self.players.insert(
                id,
                Player {
                    item: None,
                    character,
                    movement: MovementBase {
                        position: if character < 0 {
                            self.data.customer_spawn
                        } else {
                            self.data.chef_spawn
                        },
                        direction: Vec2::ZERO,
                        facing: Vec2::X,
                        rotation: 0.,
                        velocity: Vec2::ZERO,
                        boosting: false,
                        stamina: 0.,
                    },
                    last_position_update: Instant::now(),
                    boost: false,
                    direction: Vec2::ZERO,
                    communicate_persist: None,
                    interacting: None,
                    name: name.clone(),
                },
            );
        }
        packet_out.extend(self.prime_client());
    }
    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(),
                current_map: self.data.map_name.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(),
            },
        });
        out.push(PacketC::Environment {
            effects: self.environment_effects.clone(),
        });
        for (&id, player) in &self.players {
            out.push(PacketC::AddPlayer {
                id,
                position: player.movement.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),
            });
            if let Some(item) = &tdata.item {
                out.push(PacketC::SetItem {
                    location: ItemLocation::Tile(tile),
                    item: Some(item.kind),
                })
            }
        }
        out.push(PacketC::Score(self.score.clone()));
        out.push(PacketC::SetIngame {
            state: true,
            lobby: self.lobby,
        });
        out
    }
    pub fn join_player(
        &mut self,
        name: String,
        character: i32,
        packet_out: &mut VecDeque,
    ) -> PlayerID {
        let id = self.player_id_counter;
        self.player_id_counter.0 += 1;
        let position = if id.0 < 0 {
            self.data.customer_spawn
        } else {
            self.data.chef_spawn
        };
        self.players.insert(
            id,
            Player {
                item: None,
                character,
                movement: MovementBase {
                    position: if character < 0 {
                        self.data.customer_spawn
                    } else {
                        self.data.chef_spawn
                    },
                    direction: Vec2::ZERO,
                    facing: Vec2::X,
                    rotation: 0.,
                    velocity: Vec2::ZERO,
                    boosting: false,
                    stamina: 0.,
                },
                last_position_update: Instant::now(),
                boost: false,
                direction: Vec2::ZERO,
                communicate_persist: None,
                interacting: None,
                name: name.clone(),
            },
        );
        self.score.players = self.score.players.max(self.players.len());
        packet_out.push_back(PacketC::AddPlayer {
            id,
            name,
            position,
            character,
        });
        id
    }
    pub fn packet_in(
        &mut self,
        packet: PacketS,
        replies: &mut Vec,
        packet_out: &mut VecDeque,
    ) -> Result<()> {
        match packet {
            PacketS::Join { name, character } => {
                let id = self.join_player(name, character, packet_out);
                replies.push(PacketC::Joined { id })
            }
            PacketS::Leave { player } => {
                let p = self
                    .players
                    .remove(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                self.players_spatial_index.remove_entry(player);
                if let Some(item) = p.item {
                    let pos = p.movement.position.floor().as_ivec2();
                    if let Some(tile) = self.tiles.get_mut(&pos) {
                        if tile.item.is_none() {
                            packet_out.push_back(PacketC::SetItem {
                                location: ItemLocation::Tile(pos),
                                item: Some(item.kind),
                            });
                            tile.item = Some(item);
                        }
                    }
                }
                packet_out.push_back(PacketC::RemovePlayer { id: player })
            }
            PacketS::Movement {
                pos,
                boosting,
                direction,
                player,
            } => {
                let pd = self
                    .players
                    .get_mut(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                pd.direction = direction;
                pd.boost = boosting;
                if let Some(pos) = pos {
                    let dt = pd.last_position_update.elapsed();
                    pd.last_position_update += dt;
                    let diff = pos - pd.movement.position;
                    pd.movement.position += diff.clamp_length_max(dt.as_secs_f32());
                    if diff.length() > 1. {
                        replies.push(PacketC::MovementSync { player });
                    }
                }
            }
            PacketS::Interact { pos, player } => {
                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.movement.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.movement.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,
                        packet_out,
                        &mut self.score,
                        &mut self.score_changed,
                        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),
                        packet_out,
                        &mut self.score,
                        &mut self.score_changed,
                        false,
                    )
                }
            }
            PacketS::Communicate {
                message,
                persist,
                player,
            } => {
                info!("{player:?} message {message:?}");
                if persist {
                    if let Some(player) = self.players.get_mut(&player) {
                        player.communicate_persist = message.clone()
                    }
                }
                packet_out.push_back(PacketC::Communicate {
                    player,
                    message,
                    persist,
                })
            }
            PacketS::ReplaceHand { item, player } => {
                let pdata = self
                    .players
                    .get_mut(&player)
                    .ok_or(anyhow!("player does not exist"))?;
                pdata.item = item.map(|i| Item {
                    kind: i,
                    active: None,
                });
                packet_out.push_back(PacketC::SetItem {
                    location: ItemLocation::Player(player),
                    item,
                })
            }
            PacketS::ReplayTick { .. } => bail!("packet not supported in this session"),
        }
        Ok(())
    }
    /// Returns true if the game should end
    pub fn tick(&mut self, dt: f32, packet_out: &mut VecDeque) -> bool {
        if self.score_changed {
            self.score_changed = false;
            packet_out.push_back(PacketC::Score(self.score.clone()));
        }
        for (&pos, tile) in &mut self.tiles {
            if let Some(effect) = tick_slot(
                dt,
                &self.data,
                Some(tile.kind),
                &mut tile.item,
                &mut self.score,
            ) {
                match effect {
                    TickEffect::Progress(warn) => 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 => {
                        packet_out.push_back(PacketC::SetProgress {
                            warn: false,
                            item: ItemLocation::Tile(pos),
                            progress: None,
                        });
                        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 {
            player
                .movement
                .update(&self.walkable, player.direction, player.boost, dt);
            self.players_spatial_index
                .update_entry(pid, player.movement.position);
        }
        self.players_spatial_index.all(|p1, pos1| {
            self.players_spatial_index.query(pos1, 2., |p2, _pos2| {
                if let Some([a, b]) = self.players.get_many_mut([&p1, &p2]) {
                    a.movement.collide(&mut b.movement, dt)
                }
            })
        });
        for (&pid, player) in &mut self.players {
            packet_out.push_back(PacketC::Position {
                player: pid,
                pos: player.movement.position,
                boosting: player.movement.boosting,
                rot: player.movement.rotation,
            });
            if let Some(effect) = tick_slot(dt, &self.data, None, &mut player.item, &mut self.score)
            {
                match effect {
                    TickEffect::Progress(warn) => 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 => {
                        packet_out.push_back(PacketC::SetProgress {
                            warn: false,
                            item: ItemLocation::Player(pid),
                            progress: None,
                        });
                        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 player in players_auto_release.drain(..) {
            let _ = self.packet_in(
                PacketS::Interact { pos: None, player },
                &mut vec![],
                packet_out,
            );
        }
        for entity in self.entities.clone().write().unwrap().iter_mut() {
            if let Err(e) = entity.tick(self, packet_out, dt) {
                warn!("entity tick failed: {e}")
            }
        }
        let now = Instant::now();
        if let Some(end) = self.end {
            self.score.time_remaining = (end - now).as_secs_f64();
            if end < now {
                let relative_score = (self.score.points * 100) / self.data.score_baseline.max(1);
                self.score.stars = match relative_score {
                    100.. => 3,
                    70.. => 2,
                    40.. => 1,
                    _ => 0,
                };
                packet_out.push_back(PacketC::Menu(Menu::Score(self.score.clone())));
                true
            } else {
                false
            }
        } else {
            false
        }
    }
    pub fn count_chefs(&self) -> usize {
        self.players
            .values()
            .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,
    score: &mut Score,
    score_changed: &mut bool,
    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, score, 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}");
                *score_changed = true;
                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),
                    });
                }
            }
        }
    }
}
impl Player {
    pub fn position(&self) -> Vec2 {
        self.movement.position
    }
}