/*
    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::{index::GamedataIndex, DataIndex, Serverdata},
    entity::{Entities, EntityContext},
    interaction::{interact, tick_slot},
    message::TrError,
    scoreboard::ScoreboardStore,
    tre, ConnectionID,
};
use anyhow::{Context, Result};
use hurrycurry_client_lib::{Game, Involvement, Item, Player, Tile};
use hurrycurry_protocol::{
    glam::{IVec2, Vec2},
    movement::MovementBase,
    Gamedata, ItemLocation, Menu, MessageTimeout, PacketC, PacketS, PlayerClass, PlayerID, Score,
    TileIndex,
};
use log::{info, warn};
use rand::random;
use std::{
    collections::{HashMap, HashSet, VecDeque},
    sync::Arc,
    time::{Duration, Instant},
};
use tokio::sync::broadcast::Sender;
pub struct Server {
    pub tx: Sender,
    pub connections: HashMap>,
    pub game: Game,
    pub data: Arc,
    pub entities: Entities,
    pub player_id_counter: PlayerID,
    pub score_changed: bool,
    pub packet_loopback: VecDeque,
    pub last_movement_update: HashMap,
    pub index: DataIndex,
    pub packet_out: VecDeque,
    pub scoreboard: ScoreboardStore,
    pub gamedata_index: GamedataIndex,
}
pub trait GameServerExt {
    fn unload(&mut self, packet_out: &mut VecDeque);
    fn load(
        &mut self,
        gamedata: Gamedata,
        serverdata: &Serverdata,
        timer: Option,
        packet_out: &mut VecDeque,
    );
    fn join_player(
        &mut self,
        id: PlayerID,
        name: String,
        character: i32,
        class: PlayerClass,
        serverdata: &Serverdata,
        packet_out: Option<&mut VecDeque>,
    );
    fn prime_client(&self) -> Vec;
    fn set_tile(&mut self, p: IVec2, t: Option, packet_out: &mut VecDeque);
}
impl GameServerExt for Game {
    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();
    }
    fn load(
        &mut self,
        gamedata: Gamedata,
        serverdata: &Serverdata,
        timer: Option,
        packet_out: &mut VecDeque,
    ) {
        // TODO cleanup
        let players = self
            .players
            .iter()
            .filter(|(_, p)| p.class == PlayerClass::Chef)
            .map(|(id, p)| (*id, (p.name.to_owned(), p.character, p.class)))
            .collect::>();
        self.unload(packet_out);
        self.lobby = gamedata.current_map == "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);
        for (&p, (tile, item)) in &serverdata.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, class)) in players {
            self.join_player(id, name, character, class, serverdata, None);
        }
        packet_out.extend(self.prime_client());
    }
    fn prime_client(&self) -> Vec {
        let mut out = Vec::new();
        out.push(PacketC::Data {
            data: self.data.as_ref().to_owned(),
        });
        out.push(PacketC::Environment {
            effects: self.environment_effects.clone(),
        });
        for (&id, player) in &self.players {
            out.push(PacketC::AddPlayer {
                id,
                class: player.class,
                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(Involvement {
                    player,
                    position,
                    speed,
                    warn,
                    ..
                }) = item.active
                {
                    out.push(PacketC::SetProgress {
                        player,
                        item: ItemLocation::Player(id),
                        position,
                        speed,
                        warn,
                    });
                }
            }
            if let Some((message, timeout)) = &player.communicate_persist {
                out.push(PacketC::Communicate {
                    player: id,
                    message: Some(message.to_owned()),
                    timeout: Some(*timeout),
                })
            }
        }
        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),
                });
                if let Some(Involvement {
                    player,
                    position,
                    speed,
                    warn,
                    ..
                }) = item.active
                {
                    out.push(PacketC::SetProgress {
                        player,
                        item: ItemLocation::Tile(tile),
                        position,
                        speed,
                        warn,
                    });
                }
            }
        }
        out.push(PacketC::FlushMap);
        out.push(PacketC::Score(self.score.clone()));
        out.push(PacketC::SetIngame {
            state: true,
            lobby: self.lobby,
        });
        out
    }
    fn join_player(
        &mut self,
        id: PlayerID,
        name: String,
        character: i32,
        class: PlayerClass,
        serverdata: &Serverdata,
        packet_out: Option<&mut VecDeque>,
    ) {
        let position = match class {
            PlayerClass::Customer => serverdata.customer_spawn,
            PlayerClass::Bot | PlayerClass::Chef => serverdata.chef_spawn,
        } + (Vec2::new(random(), random()) - 0.5);
        self.players.insert(
            id,
            Player {
                item: None,
                character,
                class,
                movement: MovementBase::new(position),
                communicate_persist: None,
                interacting: None,
                name: name.clone(),
            },
        );
        self.score.players = self.score.players.max(self.players.len());
        if let Some(packet_out) = packet_out {
            packet_out.push_back(PacketC::AddPlayer {
                id,
                name,
                class,
                position,
                character,
            });
        }
    }
    /// Dont forget to flush
    fn set_tile(
        &mut self,
        tile: IVec2,
        kind: Option,
        packet_out: &mut VecDeque,
    ) {
        if let Some(kind) = kind {
            self.tiles.insert(tile, Tile::from(kind));
            if self.data.is_tile_colliding(kind) {
                self.walkable.remove(&tile);
            } else {
                self.walkable.insert(tile);
            }
        } else {
            self.tiles.remove(&tile);
            self.walkable.remove(&tile);
        }
        packet_out.push_back(PacketC::UpdateMap {
            tile,
            kind,
            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),
            ],
        });
    }
}
impl Server {
    pub async fn new(tx: Sender) -> Result {
        Ok(Self {
            game: Game::default(),
            index: DataIndex::load()
                .await
                .context("Failed to load data index")?,
            tx,
            packet_out: VecDeque::new(),
            connections: HashMap::new(),
            data: Serverdata::default().into(),
            gamedata_index: GamedataIndex::default(),
            entities: Vec::new(),
            player_id_counter: PlayerID(1),
            score_changed: false,
            packet_loopback: VecDeque::new(),
            last_movement_update: HashMap::default(),
            scoreboard: ScoreboardStore::load()
                .await
                .context("Failed to load scoreboards")?,
        })
    }
}
impl Server {
    pub fn load(
        &mut self,
        (gamedata, serverdata, entities): (Gamedata, Serverdata, Entities),
        timer: Option,
    ) {
        for mut e in self.entities.drain(..) {
            e.destructor(EntityContext {
                game: &mut self.game,
                packet_out: &mut self.packet_out,
                packet_in: &mut self.packet_loopback,
                score_changed: &mut self.score_changed,
                scoreboard: &self.scoreboard,
                replies: None,
                dt: 0.,
                load_map: &mut None,
            });
        }
        self.game.load(
            gamedata,
            &serverdata,
            timer.or(serverdata.default_timer),
            &mut self.packet_out,
        );
        self.gamedata_index.update(&self.game.data);
        self.data = serverdata.into();
        self.entities = entities;
    }
    pub fn packet_in(
        &mut self,
        packet: PacketS,
        replies: &mut Vec,
    ) -> Result<(), TrError> {
        match packet {
            PacketS::Join {
                name,
                character,
                id,
                class,
            } => {
                let id = id.unwrap_or_else(|| {
                    let id = self.player_id_counter;
                    self.player_id_counter.0 += 1;
                    id
                });
                self.game.join_player(
                    id,
                    name,
                    character,
                    class,
                    &self.data,
                    Some(&mut self.packet_out),
                );
                replies.push(PacketC::Joined { id })
            }
            PacketS::Leave { player } => {
                let p = self
                    .game
                    .players
                    .remove(&player)
                    .ok_or(tre!("s.error.no_player"))?;
                self.game.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.game.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::Effect { player, name } => {
                self.packet_out.push_back(PacketC::Effect { name, player });
            }
            PacketS::Movement {
                pos,
                boost,
                dir,
                player,
            } => {
                let pd = self
                    .game
                    .players
                    .get_mut(&player)
                    .ok_or(tre!("s.error.no_player"))?;
                pd.movement.input(dir, boost);
                if let Some(pos) = pos {
                    let last_position_update = self
                        .last_movement_update
                        .entry(player)
                        .or_insert_with(Instant::now);
                    let dt = last_position_update.elapsed();
                    *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 } => {
                for e in &mut self.entities {
                    if e.interact(
                        EntityContext {
                            game: &mut self.game,
                            packet_out: &mut self.packet_out,
                            packet_in: &mut self.packet_loopback,
                            score_changed: &mut self.score_changed,
                            load_map: &mut None,
                            scoreboard: &self.scoreboard,
                            replies: Some(replies),
                            dt: 0.,
                        },
                        pos,
                        player,
                    )? {
                        return Ok(());
                    }
                }
                let pid = player;
                let player = self
                    .game
                    .players
                    .get_mut(&pid)
                    .ok_or(tre!("s.error.no_player"))?;
                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(_)) => return Err(tre!("s.error.already_interacting")),
                };
                let entpos = pos.as_vec2() + Vec2::splat(0.5);
                if edge && entpos.distance(player.movement.position) > 2. {
                    return Err(tre!("s.error.interacting_too_far"));
                }
                let tile = self
                    .game
                    .tiles
                    .get_mut(&pos)
                    .ok_or(tre!("s.error.no_tile"))?;
                // No going back from here on
                player.interacting = if edge { Some(pos) } else { None };
                let other_pid = if !self.game.data.is_tile_interactable(tile.kind) {
                    self.game
                        .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
                        .game
                        .players
                        .get_many_mut([&pid, &base_pid])
                        .ok_or(tre!("s.error.self_interact"))?;
                    if this.class == PlayerClass::Customer || other.class == PlayerClass::Customer {
                        return Err(tre!("s.error.customer_interact"));
                    }
                    interact(
                        &self.game.data,
                        edge,
                        None,
                        Some(pid),
                        &mut this.item,
                        ItemLocation::Player(base_pid),
                        &mut other.item,
                        ItemLocation::Player(pid),
                        &mut self.game.score,
                        &mut self.score_changed,
                        false,
                        &mut self.packet_out,
                    )
                } else {
                    let player = self
                        .game
                        .players
                        .get_mut(&pid)
                        .ok_or(tre!("s.error.no_player"))?;
                    interact(
                        &self.game.data,
                        edge,
                        Some(tile.kind),
                        Some(pid),
                        &mut tile.item,
                        ItemLocation::Tile(pos),
                        &mut player.item,
                        ItemLocation::Player(pid),
                        &mut self.game.score,
                        &mut self.score_changed,
                        false,
                        &mut self.packet_out,
                    )
                }
            }
            PacketS::Communicate {
                message,
                timeout,
                player,
                pin,
            } => {
                info!("{player:?} message {message:?}");
                let pin = pin.unwrap_or(false);
                let timeout = if let Some(timeout) = timeout {
                    if let Some(player) = self.game.players.get_mut(&player) {
                        let mut timeout = MessageTimeout {
                            initial: timeout,
                            remaining: timeout,
                            pinned: pin,
                        };
                        if let Some((_, t)) = &player.communicate_persist {
                            timeout.initial = t.initial;
                        };
                        player.communicate_persist = message.clone().map(|m| (m, timeout.clone()));
                        Some(timeout)
                    } else {
                        None
                    }
                } else {
                    None
                };
                self.packet_out.push_back(PacketC::Communicate {
                    player,
                    message,
                    timeout,
                });
            }
            PacketS::ReplaceHand { item, player } => {
                let pdata = self.game.players.get_mut(&player).ok_or(tre!(""))?;
                pdata.item = item.map(|i| Item {
                    kind: i,
                    active: None,
                });
                self.packet_out.push_back(PacketC::SetItem {
                    location: ItemLocation::Player(player),
                    item,
                })
            }
            PacketS::ApplyScore(score) => {
                self.game.score.demands_completed += score.demands_completed;
                self.game.score.demands_failed += score.demands_failed;
                self.game.score.points += score.points;
                self.score_changed = true;
            }
            PacketS::ReplayTick { .. } => return Err(tre!("s.error.packet_not_supported")),
        }
        Ok(())
    }
    /// Returns true if the game should end
    pub fn tick(&mut self, dt: f32) -> Option<(String, Option)> {
        if self.score_changed {
            self.score_changed = false;
            self.packet_out
                .push_back(PacketC::Score(self.game.score.clone()));
        }
        for (&pos, tile) in &mut self.game.tiles {
            tick_slot(
                dt,
                &self.game.data,
                &self.gamedata_index,
                Some(tile.kind),
                &mut tile.item,
                ItemLocation::Tile(pos),
                &mut self.game.score,
                &mut self.score_changed,
                &mut self.packet_out,
            );
        }
        for (&pid, player) in &mut self.game.players {
            player.movement.update(&self.game.walkable, dt);
            self.game
                .players_spatial_index
                .update_entry(pid, player.movement.position);
        }
        self.game.players_spatial_index.all(|p1, pos1| {
            self.game
                .players_spatial_index
                .query(pos1, 2., |p2, _pos2| {
                    if let Some([a, b]) = self.game.players.get_many_mut([&p1, &p2]) {
                        a.movement.collide(&mut b.movement, dt)
                    }
                })
        });
        for (&pid, player) in &mut self.game.players {
            self.packet_out.push_back(PacketC::Movement {
                player: pid,
                pos: player.movement.position,
                dir: player.movement.input_direction,
                boost: player.movement.boosting,
                rot: player.movement.rotation,
            });
            tick_slot(
                dt,
                &self.game.data,
                &self.gamedata_index,
                None,
                &mut player.item,
                ItemLocation::Player(pid),
                &mut self.game.score,
                &mut self.score_changed,
                &mut self.packet_out,
            );
        }
        let mut players_auto_release = Vec::new();
        for (pid, player) in &mut self.game.players {
            if let Some((_, timeout)) = &mut player.communicate_persist {
                timeout.remaining -= dt;
                if timeout.remaining < 0. {
                    player.communicate_persist = None;
                }
            }
            if let Some(pos) = player.interacting {
                if let Some(tile) = self.game.tiles.get(&pos) {
                    if let Some(item) = &tile.item {
                        if let Some(involvement) = &item.active {
                            if involvement.position >= 1. {
                                players_auto_release.push(*pid);
                            }
                        }
                    }
                }
            }
        }
        for player in players_auto_release.drain(..) {
            let _ = self.packet_in(PacketS::Interact { pos: None, player }, &mut vec![]);
        }
        let mut load_map = None;
        for entity in self.entities.iter_mut() {
            if let Err(e) = entity.tick(EntityContext {
                game: &mut self.game,
                load_map: &mut load_map,
                packet_out: &mut self.packet_out,
                score_changed: &mut self.score_changed,
                packet_in: &mut self.packet_loopback,
                scoreboard: &self.scoreboard,
                replies: None,
                dt,
            }) {
                warn!("Entity tick failed: {e}")
            }
        }
        self.entities.retain_mut(|e| {
            if e.finished() {
                e.destructor(EntityContext {
                    game: &mut self.game,
                    load_map: &mut load_map,
                    packet_out: &mut self.packet_out,
                    score_changed: &mut self.score_changed,
                    packet_in: &mut self.packet_loopback,
                    scoreboard: &self.scoreboard,
                    replies: None,
                    dt: 0.,
                });
                false
            } else {
                true
            }
        });
        if let Some(map) = load_map {
            return Some((map, None));
        }
        while let Some(p) = self.packet_loopback.pop_front() {
            if let Err(e) = self.packet_in(p, &mut vec![]) {
                warn!("Internal packet errored: {e:?}");
            }
        }
        let now = Instant::now();
        if let Some(end) = self.game.end {
            self.game.score.time_remaining = (end - now).as_secs_f64();
            if end < now {
                let relative_score =
                    (self.game.score.points * 100) / self.data.score_baseline.max(1);
                self.game.score.stars = match relative_score {
                    100.. => 3,
                    70.. => 2,
                    40.. => 1,
                    _ => 0,
                };
                self.packet_out
                    .push_back(PacketC::Menu(Menu::Score(self.game.score.clone())));
                self.scoreboard.insert(
                    &self.game.data.current_map,
                    self.game
                        .players
                        .values()
                        .filter(|m| m.class == PlayerClass::Chef)
                        .map(|m| m.name.clone())
                        .collect(),
                    self.game.score.clone(),
                );
                Some(("lobby".to_string(), None))
            } else {
                None
            }
        } else {
            None
        }
    }
    pub fn count_chefs(&self) -> usize {
        self.game
            .players
            .values()
            .map(|p| if p.class == PlayerClass::Chef { 1 } else { 0 })
            .sum()
    }
}