/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2025 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 network;
pub mod spatial_index;

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

use crate::gamedata_index::GamedataIndex;

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

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

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

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

    pub movement: MovementBase,
}

#[derive(Default)]
pub struct Game {
    pub data: Arc<Gamedata>,
    pub data_index: GamedataIndex,
    pub tiles: HashMap<IVec2, Tile>,
    pub walkable: HashSet<IVec2>,
    pub players: HashMap<PlayerID, Player>,
    pub players_spatial_index: SpatialIndex<PlayerID>,
    pub end: Option<Instant>,
    pub lobby: bool,

    pub environment_effects: HashSet<String>,
    pub score: Score,

    pub player_id_counter: i64,
}

impl Game {
    pub fn apply_packet(&mut self, packet: PacketC) {
        match packet {
            PacketC::Data { 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),
                    },
                );
            }
            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,
                player,
                speed,
                warn,
            } => {
                if let Some(Some(item)) = self.get_item(item) {
                    item.active = Some(Involvement {
                        player,
                        speed,
                        warn,
                        position,
                        recipe: RecipeIndex(0),
                    });
                }
            }
            PacketC::UpdateMap {
                tile,
                kind,
                neighbors: _,
            } => {
                if let Some(kind) = kind {
                    self.tiles.insert(tile, Tile { kind, item: None });
                    if self.data_index.tile_collide[kind.0] {
                        self.walkable.remove(&tile);
                    } else {
                        self.walkable.insert(tile);
                    }
                } else {
                    self.tiles.remove(&tile);
                    self.walkable.remove(&tile);
                }
            }
            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 tick(&mut self, dt: f32) {
        self.score.time_remaining -= dt as f64;
        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)
    }
}

impl From<TileIndex> for Tile {
    fn from(kind: TileIndex) -> Self {
        Self { kind, item: None }
    }
}
