/*
    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/>.

*/
use super::{Entity, EntityContext};
use anyhow::Result;
use hurrycurry_locale::TrError;
use hurrycurry_protocol::{Effect, Hand};
use hurrycurry_protocol::{ItemIndex, ItemLocation, Message, PacketC, PlayerID, glam::IVec2};
use std::collections::{HashMap, HashSet};
use std::fmt::Write;

#[derive(Debug, Clone)]
pub struct CtfMinigame {
    ready: bool,
    time: f32,
    teams: HashMap<ItemIndex, TeamData>,
}
#[derive(Debug, Clone)]
struct TeamData {
    spawn: IVec2,
    players: HashSet<PlayerID>,
    score: isize,
}

impl TeamData {
    fn effect(&self, c: &mut EntityContext, effect: Effect) -> Result<(), TrError> {
        for pid in self.players.iter() {
            c.packet_out.push_back(PacketC::Effect {
                effect: effect.clone(),
                location: ItemLocation::Player(*pid, Hand(0)),
            });
        }
        Ok(())
    }
}

impl CtfMinigame {
    pub fn new(spawnpoints: &[IVec2], items: &[ItemIndex]) -> Self {
        Self {
            ready: false,
            teams: items
                .iter()
                .zip(spawnpoints.iter())
                .map(|(&item, &spawn)| {
                    (
                        item,
                        TeamData {
                            spawn,
                            players: HashSet::new(),
                            score: 0,
                        },
                    )
                })
                .collect(),
            time: 0.,
        }
    }

    fn setup(&mut self, c: &mut EntityContext) -> Result<(), TrError> {
        let mut players = c.game.players.iter_mut();
        'a: loop {
            for (_, data) in self.teams.iter_mut() {
                if let Some((&pid, player)) = players.next() {
                    data.players.insert(pid);
                    player.movement.position = data.spawn.as_vec2();
                } else {
                    break 'a;
                }
            }
        }
        for (i, team) in self.teams.iter() {
            c.game.set_item(team.spawn, Some(*i));
        }
        self.ready = true;
        Ok(())
    }

    fn get_team(&mut self, pid: PlayerID) -> Result<ItemIndex, TrError> {
        Ok(*self
            .teams
            .iter_mut()
            .find(|(_, d)| d.players.contains(&pid))
            .ok_or(TrError::Plain("Player is not in any team".to_string()))?
            .0)
    }

    fn return_flag(&mut self, c: &mut EntityContext, team_idx: ItemIndex) -> Result<(), TrError> {
        let team = self.teams.get_mut(&team_idx).unwrap();
        for (&pos, tile) in c.game.tiles.iter_mut() {
            if tile.item.as_ref().is_some_and(|a| a.kind == team_idx) {
                tile.item = None;
                c.packet_out.push_back(PacketC::SetItem {
                    location: ItemLocation::Tile(pos),
                    item: None,
                });
            }
        }
        for (pid, player) in c.game.players.iter_mut() {
            for (hand, item) in player
                .items
                .iter_mut()
                .enumerate()
                .filter(|(_, i)| i.as_ref().is_some_and(|a| a.kind == team_idx))
            {
                *item = None;
                c.packet_out.push_back(PacketC::SetItem {
                    location: ItemLocation::Player(*pid, hurrycurry_protocol::Hand(hand)),
                    item: None,
                });
            }
        }
        c.game.set_item(team.spawn, Some(team_idx));
        Ok(())
    }

    fn return_player(&mut self, c: &mut EntityContext, pid: PlayerID) -> Result<(), TrError> {
        let (_, team) = self
            .teams
            .iter()
            .find(|(_, d)| d.players.contains(&pid))
            .ok_or(TrError::Plain("Player is not in any team".to_string()))?;
        c.game
            .players
            .get_mut(&pid)
            .ok_or(TrError::Plain("Player is missing".to_string()))?
            .movement
            .position = team.spawn.as_vec2();
        Ok(())
    }

    fn player_flags(
        &mut self,
        c: &mut EntityContext,
        pid: PlayerID,
    ) -> Result<Vec<ItemIndex>, TrError> {
        Ok(c.game
            .players
            .get_mut(&pid)
            .ok_or(TrError::Plain("Player is missing".to_string()))?
            .items
            .iter()
            .flatten()
            .map(|a| a.kind)
            .filter(|a| self.teams.contains_key(a))
            .collect::<Vec<_>>())
    }

    fn send_table(&self, c: &mut EntityContext) -> Result<(), TrError> {
        let mut o = String::new();
        let ti = self
            .teams
            .iter()
            .map(|(i, d)| (i, (d.score, d.players.iter().collect::<Vec<_>>())));
        writeln!(o, "Capture the Curry! — Get the other team's item!").unwrap();
        write!(o, "|").unwrap();
        for (item, _) in ti.clone() {
            write!(o, "{:<20}|", c.game.data.item_name(*item)).unwrap();
        }
        writeln!(o).unwrap();
        write!(o, "|").unwrap();
        for (_, (s, _)) in ti.clone() {
            write!(o, "{:<20}|", s).unwrap();
        }
        writeln!(o).unwrap();
        write!(o, "|").unwrap();
        for _ in ti.clone() {
            write!(o, "{:-<20}|", "").unwrap();
        }
        writeln!(o).unwrap();
        let max = self
            .teams
            .iter()
            .map(|a| a.1.players.len())
            .max()
            .ok_or(TrError::Plain(
                "There must be at least one team".to_string(),
            ))?;
        for i in 0..max {
            write!(o, "|").unwrap();
            for (_, (_, team)) in ti.clone() {
                if let Some(p) = team.get(i).and_then(|&i| c.game.players.get(i)) {
                    write!(o, "{:<20}|", p.name).unwrap();
                } else {
                    write!(o, "{:<20}|", "").unwrap();
                }
            }
            writeln!(o).unwrap();
        }
        c.packet_out.push_back(PacketC::ServerMessage {
            error: false,
            message: Message::Text(o),
        });
        Ok(())
    }
}

impl Entity for CtfMinigame {
    fn tick(&mut self, mut c: EntityContext) -> Result<(), TrError> {
        if !self.ready {
            self.setup(&mut c)?;
        }
        for (_, team) in self.teams.iter_mut() {
            team.players.retain(|pid| c.game.players.contains_key(pid));
        }
        self.time += c.dt;
        while self.time > 0. {
            self.send_table(&mut c).unwrap();
            self.time -= 1.
        }
        Ok(())
    }
    fn interact(
        &mut self,
        mut c: EntityContext<'_>,
        target: Option<ItemLocation>,
        from: PlayerID,
    ) -> Result<bool, TrError> {
        if let Some(target) = target {
            match target {
                ItemLocation::Tile(pos) => {
                    if let Some(to_team_idx) = self
                        .teams
                        .iter()
                        .find(|(_, d)| d.spawn == pos)
                        .map(|a| *a.0)
                    {
                        let from_team_idx = self.get_team(from)?;

                        if to_team_idx != from_team_idx {
                            Ok(false)
                        } else {
                            if c.game
                                .players
                                .get(&from)
                                .unwrap()
                                .items
                                .iter()
                                .flatten()
                                .any(|i| i.kind != from_team_idx)
                                && c.game
                                    .tiles
                                    .get(&pos)
                                    .and_then(|a| a.item.as_ref().map(|a| a.kind))
                                    .is_some_and(|a| a == from_team_idx)
                            {
                                for i in self.player_flags(&mut c, from)? {
                                    self.teams.get(&i).unwrap().effect(&mut c, Effect::Angry)?;
                                    self.return_flag(&mut c, i)?;
                                    self.teams.get_mut(&from_team_idx).unwrap().score += 10;
                                }
                                self.teams
                                    .get(&from_team_idx)
                                    .unwrap()
                                    .effect(&mut c, Effect::Satisfied)?;
                                self.send_table(&mut c)?;
                            }
                            Ok(true)
                        }
                    } else {
                        Ok(false)
                    }
                }
                ItemLocation::Player(to, _) => {
                    let from_team_idx = self.get_team(from)?;
                    let to_team_idx = self.get_team(to)?;
                    if from_team_idx != to_team_idx {
                        for i in self.player_flags(&mut c, to)? {
                            self.return_flag(&mut c, i)?;
                            self.teams.get_mut(&from_team_idx).unwrap().score += 3;
                        }
                        self.send_table(&mut c)?;
                        self.return_player(&mut c, to)?;
                        Ok(true)
                    } else {
                        Ok(false)
                    }
                }
            }
        } else {
            Ok(false)
        }
    }
}
