/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2026 Hurry Curry! Contributors

    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 <https://www.gnu.org/licenses/>.

*/
pub mod gamedata_index;
pub mod interaction;
pub mod network;
pub mod spatial_index;

use crate::gamedata_index::GamedataIndex;
use hurrycurry_protocol::{
    Character, GameConfig, Gamedata, Hand, ItemIndex, ItemLocation, Message, MessageTimeout,
    PacketC, PlayerClass, PlayerID, RecipeIndex, Score, TileIndex, glam::IVec2,
    movement::MovementBase,
};
use spatial_index::SpatialIndex;
use std::{
    collections::{BTreeSet, HashMap, HashSet, VecDeque},
    sync::Arc,
};

#[derive(Debug, Clone, PartialEq)]
pub struct Involvement {
    pub position: f32,
    pub speed: f32,
    pub recipe: RecipeIndex,
    pub players: BTreeSet<PlayerID>,
    pub warn: bool,
}

#[derive(Debug, PartialEq)]
pub struct Item {
    pub kind: ItemIndex,
    pub active: Option<Involvement>,
}

pub struct Tile {
    pub parts: Vec<TileIndex>,
    pub item: Option<Item>,
}

pub struct Player {
    pub name: String,
    pub class: PlayerClass,
    pub character: Character,
    pub interacting: Option<(ItemLocation, Hand)>,
    pub items: Vec<Option<Item>>,
    pub communicate_persist: Option<(Message, MessageTimeout)>,

    pub movement: MovementBase,
    pub movement_input_changed: bool,
    pub movement_must_sync: bool,
}

#[derive(Default)]
pub struct Game {
    pub config: GameConfig,
    pub data: Arc<Gamedata>,
    pub data_index: GamedataIndex,

    pub tiles: HashMap<IVec2, Tile>,
    pub players: HashMap<PlayerID, Player>,
    pub lobby: bool,
    pub environment_effects: HashSet<String>,
    pub score: Score,
    pub player_id_counter: i64,

    pub players_spatial_index: SpatialIndex<PlayerID>,
    pub walkable: HashSet<IVec2>,
    pub walkable_preferred: HashSet<IVec2>, // bots try to avoid leaving these
    pub tile_index: HashMap<TileIndex, HashSet<IVec2>>,
    pub item_locations_index: HashSet<ItemLocation>,

    pub events: VecDeque<PacketC>,
    pub map_changes: HashSet<IVec2>,
}

impl Game {
    pub fn apply_packet(&mut self, packet: PacketC) {
        match packet {
            PacketC::GameData(data) => {
                self.data = Arc::new(*data);
            }
            PacketC::AddPlayer {
                id,
                position,
                character,
                class,
                name,
            } => {
                self.players.insert(
                    id,
                    Player {
                        name,
                        character,
                        class,
                        interacting: None,
                        items: (0..self.data.hand_count).map(|_| None).collect(),
                        communicate_persist: None,
                        movement: MovementBase::new(position),
                        movement_input_changed: false,
                        movement_must_sync: false,
                    },
                );
            }
            PacketC::RemovePlayer { id } => {
                self.players.remove(&id);
            }
            PacketC::Movement {
                player,
                pos,
                rot,
                boost,
                dir,
                ..
            } => {
                if let Some(p) = self.players.get_mut(&player) {
                    p.movement.input(dir, boost);
                    p.movement.position = pos;
                    p.movement.rotation = rot;
                }
            }
            PacketC::MoveItem { from, to } => {
                if let Some(item) = self.get_item(to).map(|e| e.take()) {
                    if let Some(to) = self.get_item(from) {
                        *to = item;
                    } else {
                        // TODO perhaps restore to original position?
                    }
                }
            }
            PacketC::SetItem { location, item } => {
                let location = self.get_item(location);
                if let Some(location) = location {
                    *location = item.map(|kind| Item { kind, active: None });
                }
            }
            PacketC::ClearProgress { item } => {
                if let Some(Some(item)) = self.get_item(item) {
                    item.active = None;
                }
            }
            PacketC::SetProgress {
                item,
                position,
                players,
                speed,
                warn,
            } => {
                if let Some(Some(item)) = self.get_item(item) {
                    item.active = Some(Involvement {
                        players,
                        speed,
                        warn,
                        position,
                        recipe: RecipeIndex(0),
                    });
                }
            }
            PacketC::UpdateMap { changes } => {
                for (pos, tiles) in changes {
                    self.set_tile(pos, tiles);
                }
            }
            PacketC::Communicate {
                player,
                message,
                timeout,
            } => {
                if let Some(timeout) = &timeout
                    && let Some(player) = self.players.get_mut(&player)
                {
                    player.communicate_persist = message.to_owned().map(|m| (m, *timeout));
                }
            }
            PacketC::Score(score) => {
                self.score = score;
            }
            PacketC::SetIngame { state: _, lobby } => {
                self.lobby = lobby;
            }
            PacketC::Environment { effects } => {
                self.environment_effects = effects;
            }
            _ => (),
        }
    }

    pub fn set_tile(&mut self, pos: IVec2, parts: Vec<TileIndex>) {
        self.tiles.remove(&pos);
        if let Some(prev) = self.tiles.get(&pos) {
            for &part in &prev.parts {
                self.tile_index.entry(part).or_default().remove(&pos);
            }
        }
        self.update_tile_changed(pos, &parts);
        for &part in &parts {
            self.tile_index.entry(part).or_default().insert(pos);
        }
        if !parts.is_empty() {
            self.tiles.insert(pos, Tile { parts, item: None });
        }
    }
    pub fn add_tile_part(&mut self, pos: IVec2, part: TileIndex) {
        let tile = self.tiles.entry(pos).or_insert(Tile {
            item: None,
            parts: vec![],
        });
        if tile.parts.contains(&part) {
            return;
        }
        tile.parts.push(part);
        let parts = tile.parts.clone();
        self.update_tile_changed(pos, &parts);
        self.tile_index.entry(part).or_default().insert(pos);
    }
    pub fn remove_tile_part(&mut self, pos: IVec2, part: TileIndex) {
        let Some(tile) = self.tiles.get_mut(&pos) else {
            return;
        };
        let lb = tile.parts.len();
        tile.parts.retain(|&p| p != part);
        if tile.parts.len() == lb {
            return;
        }
        let parts = tile.parts.clone();
        self.update_tile_changed(pos, &parts);
        self.tile_index.entry(part).or_default().remove(&pos);
    }
    fn update_tile_changed(&mut self, pos: IVec2, parts: &[TileIndex]) {
        self.map_changes.insert(pos);
        self.walkable.remove(&pos);
        self.walkable_preferred.remove(&pos);
        if self.data.walkable(&parts) {
            self.walkable.insert(pos);
        }
        if self.data.walkable_preferred(&parts) {
            self.walkable_preferred.insert(pos);
        }
    }

    pub fn set_item(&mut self, pos: IVec2, kind: Option<ItemIndex>) {
        let Some(tile) = self.tiles.get_mut(&pos) else {
            return;
        };
        if kind.is_some() {
            self.item_locations_index.insert(ItemLocation::Tile(pos));
        } else {
            self.item_locations_index.remove(&ItemLocation::Tile(pos));
        }
        tile.item = kind.map(|kind| Item { kind, active: None });
        self.events.push_back(PacketC::SetItem {
            location: ItemLocation::Tile(pos),
            item: kind,
        });
    }

    pub fn tick(&mut self, dt: f32) {
        self.score.time_remaining -= dt;
        self.score.time_remaining -= self.score.time_remaining.max(0.);

        for (&pid, player) in &mut self.players {
            player.movement.update(&self.walkable, dt);
            if let Some((_, timeout)) = &mut player.communicate_persist {
                timeout.remaining -= dt;
                if timeout.remaining < 0. {
                    player.communicate_persist = None;
                }
            }
            self.players_spatial_index
                .update_entry(pid, player.movement.position);
        }

        for player in self.players.values_mut() {
            for item in player.items.iter_mut().flatten() {
                if let Some(active) = &mut item.active {
                    active.position += active.speed;
                }
            }
        }
        for tile in self.tiles.values_mut() {
            if let Some(item) = &mut tile.item
                && let Some(active) = &mut item.active
            {
                active.position += active.speed;
            }
        }

        self.players_spatial_index.all(|p1, pos1| {
            self.players_spatial_index.query(pos1, 2., |p2, _pos2| {
                if let [Some(a), Some(b)] = self.players.get_disjoint_mut([&p1, &p2]) {
                    a.movement.collide(&mut b.movement, dt);
                }
            })
        });
    }

    pub fn get_item(&mut self, location: ItemLocation) -> Option<&mut Option<Item>> {
        match location {
            ItemLocation::Tile(pos) => Some(&mut self.tiles.get_mut(&pos)?.item),
            ItemLocation::Player(pid, hand) => {
                Some(self.players.get_mut(&pid)?.items.get_mut(hand.0)?)
            }
        }
    }
    pub fn get_unused_player_id(&mut self) -> PlayerID {
        //! not possible because of join logic in server / multiple entities spawning bots
        // let mut id = PlayerID(0);
        // while self.players.contains_key(&id) {
        //     id.0 += 1;
        // }
        // id
        self.player_id_counter += 1;
        PlayerID(self.player_id_counter)
    }

    pub fn prime_client(&self) -> Vec<PacketC> {
        let mut out = Vec::new();
        out.push(PacketC::GameData({
            let mut k = self.data.as_ref().to_owned();
            k.recipes.clear();
            k.demands.clear();
            Box::new(k)
        }));
        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(),
            });
            for (i, item) in player.items.iter().enumerate() {
                if let Some(item) = &item {
                    out.push(PacketC::SetItem {
                        location: ItemLocation::Player(id, Hand(i)),
                        item: Some(item.kind),
                    });
                    if let Some(Involvement {
                        players: player,
                        position,
                        speed,
                        warn,
                        ..
                    }) = item.active.clone()
                    {
                        out.push(PacketC::SetProgress {
                            players: player,
                            item: ItemLocation::Player(id, Hand(i)),
                            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),
                })
            }
        }
        out.push(PacketC::UpdateMap {
            changes: self
                .tiles
                .iter()
                .map(|(pos, tile)| (*pos, tile.parts.clone()))
                .collect(),
        });
        for (&tile, tdata) in &self.tiles {
            if let Some(item) = &tdata.item {
                out.push(PacketC::SetItem {
                    location: ItemLocation::Tile(tile),
                    item: Some(item.kind),
                });
                if let Some(Involvement {
                    players,
                    position,
                    speed,
                    warn,
                    ..
                }) = item.active.clone()
                {
                    out.push(PacketC::SetProgress {
                        players: players.to_owned(),
                        item: ItemLocation::Tile(tile),
                        position,
                        speed,
                        warn,
                    });
                }
            }
        }
        out.push(PacketC::Score(self.score.clone()));
        out.push(PacketC::SetIngame {
            state: true,
            lobby: self.lobby,
        });
        out
    }

    pub fn unload(&mut self, packet_out: &mut VecDeque<PacketC>) {
        packet_out.push_back(PacketC::SetIngame {
            state: false,
            lobby: false,
        });
        for (id, _) in self.players.drain() {
            self.players_spatial_index.remove_entry(id);
            packet_out.push_back(PacketC::RemovePlayer { id })
        }
        packet_out.push_back(PacketC::UpdateMap {
            changes: self.tiles.keys().map(|&p| (p, Vec::new())).collect(),
        });
        self.tiles.clear();
        self.score = Score::default();
        self.environment_effects.clear();
        self.walkable.clear();
        self.walkable_preferred.clear();
        self.tile_index.clear();
        self.item_locations_index.clear();
    }

    pub fn remove_player(&mut self, id: PlayerID) -> bool {
        let Some(p) = self.players.remove(&id) else {
            return false;
        };
        for (hand, item) in p.items.into_iter().enumerate() {
            if let Some(item) = item {
                self.item_locations_index
                    .remove(&ItemLocation::Player(id, Hand(hand)));
                let pos = p.movement.position.floor().as_ivec2();
                if let Some(tile) = self.tiles.get_mut(&pos)
                    && tile.item.is_none()
                {
                    self.set_item(pos, Some(item.kind));
                }
            }
        }
        self.events.push_back(PacketC::RemovePlayer { id });
        true
    }
}
