/*
    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::DataIndex,
    entity::bot::BotDriver,
    server::{Server, ServerState},
    ConnectionID,
};
use anyhow::{anyhow, bail, Result};
use clap::{Parser, ValueEnum};
use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
use hurrycurry_client_lib::Game;
use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID};
use log::{debug, trace};
use std::{
    collections::{HashMap, HashSet, VecDeque},
    time::Duration,
};
use tokio::sync::broadcast::Sender;
pub struct State {
    index: DataIndex,
    packet_out: VecDeque,
    tx: Sender,
    connections: HashMap>,
    pub server: ServerState,
    pub game: Game,
}
#[derive(Parser)]
#[clap(multicall = true)]
enum Command {
    /// Start a new game
    Start {
        /// Gamedata specification
        #[arg(default_value = "junior")]
        spec: String,
        /// Duration in seconds
        #[arg(default_value = "420")]
        timer: u64,
    },
    /// Abort the current game
    End,
    /// Download recipe/map's source declaration
    Download {
        /// Resource kind
        #[arg(value_enum)]
        r#type: DownloadType,
        /// Name
        name: String,
    },
    /// List all recipes and maps
    List,
    /// Send an effect
    Effect { name: String },
    /// Send an item
    Item { name: String },
    /// Reload the resource index
    ReloadIndex,
    #[clap(alias = "summon-bot", alias = "spawn-bot")]
    CreateBot { algo: String, name: Option },
    /// Reload the current map
    #[clap(alias = "r")]
    Reload,
}
#[derive(ValueEnum, Clone)]
enum DownloadType {
    Map,
    Recipes,
}
impl State {
    pub async fn new(tx: Sender) -> Result {
        let mut index = DataIndex::default();
        index.reload()?;
        let mut packet_out = VecDeque::new();
        let mut game = Game::default();
        let mut server = ServerState::default();
        {
            Server {
                game: &mut game,
                state: &mut server,
            }
            .load(
                index.generate("lobby-none".to_string()).await?,
                None,
                &mut packet_out,
            );
        }
        Ok(Self {
            game,
            server,
            index,
            tx,
            packet_out,
            connections: HashMap::new(),
        })
    }
    pub async fn tick(&mut self, dt: f32) -> anyhow::Result<()> {
        let mut server = Server {
            game: &mut self.game,
            state: &mut self.server,
        };
        if server.tick(dt, &mut self.packet_out) {
            server.load(
                self.index.generate("lobby-none".to_string()).await?,
                None,
                &mut self.packet_out,
            );
        }
        while let Some(p) = self.packet_out.pop_front() {
            if matches!(p, PacketC::UpdateMap { .. } | PacketC::Movement { .. }) {
                trace!("-> {p:?}");
            } else {
                debug!("-> {p:?}");
            }
            self.tx.send(p).unwrap();
        }
        Ok(())
    }
    pub async fn packet_in(&mut self, conn: ConnectionID, packet: PacketS) -> Result> {
        if let Some(p) = get_packet_player(&packet) {
            if !self.connections.entry(conn).or_default().contains(&p) {
                bail!("Packet sent to player that is not owned by this connection.");
            }
        }
        let mut replies = Vec::new();
        match &packet {
            PacketS::Communicate {
                message: Some(Message::Text(text)),
                timeout: None,
                player,
            } if let Some(command) = text.strip_prefix("/") => {
                match self.handle_command_parse(*player, command).await {
                    Ok(()) => return Ok(vec![]),
                    Err(e) => {
                        return Ok(vec![PacketC::ServerMessage {
                            text: format!("{e}"),
                        }]);
                    }
                }
            }
            PacketS::Leave { player } => {
                self.connections.entry(conn).or_default().remove(player);
            }
            PacketS::Join { .. } => {
                if self.connections.entry(conn).or_default().len() > 8 {
                    bail!("Players per connection limit exceeded.")
                }
            }
            _ => (),
        }
        let mut server = Server {
            game: &mut self.game,
            state: &mut self.server,
        };
        server.packet_in(packet, &mut replies, &mut self.packet_out)?;
        for p in &replies {
            match p {
                PacketC::Joined { id } => {
                    self.connections.entry(conn).or_default().insert(*id);
                }
                _ => (),
            }
        }
        if server.count_chefs() <= 0 && !server.game.lobby {
            self.tx
                .send(PacketC::ServerMessage {
                    text: "Game was aborted automatically due to a lack of players".to_string(),
                })
                .ok();
            server.load(
                self.index.generate("lobby-none".to_string()).await?,
                None,
                &mut self.packet_out,
            );
        }
        Ok(replies)
    }
    pub async fn disconnect(&mut self, conn: ConnectionID) {
        if let Some(players) = self.connections.get(&conn) {
            for player in players.to_owned() {
                let _ = self.packet_in(conn, PacketS::Leave { player }).await;
            }
        }
        self.connections.remove(&conn);
    }
    async fn handle_command_parse(&mut self, player: PlayerID, command: &str) -> Result<()> {
        for line in command.split("\n") {
            self.handle_command(
                player,
                Command::try_parse_from(
                    shlex::split(line)
                        .ok_or(anyhow!("quoting invalid"))?
                        .into_iter(),
                )?,
            )
            .await?;
        }
        Ok(())
    }
    async fn handle_command(&mut self, player: PlayerID, command: Command) -> Result<()> {
        let mut server = Server {
            game: &mut self.game,
            state: &mut self.server,
        };
        match command {
            Command::Start { spec, timer } => {
                let data = self.index.generate(spec).await?;
                server.load(data, Some(Duration::from_secs(timer)), &mut self.packet_out);
            }
            Command::End => {
                self.tx
                    .send(PacketC::ServerMessage {
                        text: format!(
                            "Game was aborted by {}.",
                            server
                                .game
                                .players
                                .get(&player)
                                .ok_or(anyhow!("player missing"))?
                                .name
                        ),
                    })
                    .ok();
                server.load(
                    self.index.generate("lobby-none".to_string()).await?,
                    None,
                    &mut self.packet_out,
                );
            }
            Command::Reload => {
                if server.count_chefs() > 1 {
                    bail!("must be at most one player to reload");
                }
                server.load(
                    self.index
                        .generate(server.state.data.spec.to_string())
                        .await?,
                    None,
                    &mut self.packet_out,
                );
            }
            Command::ReloadIndex => {
                self.index.reload()?;
            }
            Command::Download { r#type, name } => {
                let source = match r#type {
                    DownloadType::Map => self.index.read_map(&name).await,
                    DownloadType::Recipes => self.index.read_recipes(&name).await,
                }?;
                bail!("{source}");
            }
            Command::List => {
                bail!(
                    "Maps: {:?}\nRecipes: {:?}",
                    self.index.maps.keys().collect::>(),
                    self.index.recipes
                )
            }
            Command::Effect { name } => {
                self.tx
                    .send(PacketC::Communicate {
                        player,
                        message: Some(Message::Effect(name)),
                        timeout: None,
                    })
                    .ok();
            }
            Command::Item { name } => {
                let item = server
                    .game
                    .data
                    .get_item_by_name(&name)
                    .ok_or(anyhow!("unknown item"))?;
                self.tx
                    .send(PacketC::Communicate {
                        player,
                        message: Some(Message::Item(item)),
                        timeout: None,
                    })
                    .ok();
            }
            Command::CreateBot { algo, name } => {
                let (aname, cons) = ALGO_CONSTRUCTORS
                    .iter()
                    .find(|(name, _)| *name == algo.as_str())
                    .ok_or(anyhow!("algo name unknown"))?;
                let algo = cons();
                self.server.entities.push(Box::new(BotDriver::new(
                    format!("{}-bot", name.unwrap_or((*aname).to_owned())),
                    51,
                    algo,
                )));
            }
        }
        Ok(())
    }
}
fn get_packet_player(packet: &PacketS) -> Option {
    match packet {
        PacketS::Join { .. } => None,
        PacketS::Leave { player } => Some(*player),
        PacketS::Movement { player, .. } => Some(*player),
        PacketS::Interact { player, .. } => Some(*player),
        PacketS::Communicate { player, .. } => Some(*player),
        PacketS::ReplaceHand { player, .. } => Some(*player),
        PacketS::ApplyScore(_) => None,
        PacketS::ReplayTick { .. } => None,
    }
}