/*
    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::{
    entity::{bot::BotDriver, tutorial::Tutorial},
    message::TrError,
    server::Server,
    tre, trm,
};
use anyhow::Result;
use clap::{Parser, ValueEnum};
use hurrycurry_bot::algos::ALGO_CONSTRUCTORS;
use hurrycurry_protocol::{DocumentElement, Menu, Message, PacketC, PlayerClass, PlayerID};
use std::{fmt::Write, time::Duration};
#[derive(Parser)]
#[clap(multicall = true)]
enum Command {
    /// Start a new game
    Start {
        /// Gamedata specification
        #[arg(default_value = "junior")]
        spec: String,
        /// Duration in seconds
        timer: Option,
    },
    /// Shows the best entries of the scoreboard for this map.
    #[clap(alias = "top5")]
    Top {
        /// Name of the map, default: current
        map: Option,
        /// Send text instead of document
        #[arg(short, long)]
        text: bool,
    },
    #[clap(alias = "mapinfo")]
    Info {
        /// Name of the map, default: current
        map: Option,
    },
    /// 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", alias = "bot")]
    CreateBot {
        algo: String,
        name: Option,
    },
    /// Reload the current map
    #[clap(alias = "r")]
    Reload,
    /// Shows the recipe book
    Book,
    #[clap(alias = "tutorial")]
    StartTutorial {
        item: String,
    },
    EndTutorial,
    #[clap(alias = "tr")]
    TranslateMessage {
        message_id: String,
        arguments: Vec,
    },
}
#[derive(ValueEnum, Clone)]
enum DownloadType {
    Map,
    Recipes,
}
impl Server {
    pub async fn handle_command_parse(
        &mut self,
        player: PlayerID,
        command: &str,
    ) -> Result, TrError> {
        let mut replies = Vec::new();
        for line in command.split("\n") {
            self.handle_command(
                player,
                Command::try_parse_from(
                    shlex::split(line)
                        .ok_or(tre!("s.error.quoting_invalid"))?
                        .into_iter(),
                )
                .map_err(|e| TrError::Plain(e.to_string()))?,
                &mut replies,
            )
            .await?;
        }
        Ok(replies)
    }
    async fn handle_command(
        &mut self,
        player: PlayerID,
        command: Command,
        replies: &mut Vec,
    ) -> Result<(), TrError> {
        match command {
            Command::Start { spec, timer } => {
                if !self.game.lobby {
                    self.tx
                        .send(PacketC::ServerMessage {
                            message: trm!(
                                "s.state.game_aborted",
                                s = self
                                    .game
                                    .players
                                    .get(&player)
                                    .ok_or(tre!("s.error.no_player"))?
                                    .name
                                    .clone()
                            ),
                            error: false,
                        })
                        .ok();
                }
                let data = self
                    .index
                    .generate(&spec)
                    .await
                    .map_err(|e| TrError::Plain(e.to_string()))?;
                self.load(data, timer.map(Duration::from_secs));
            }
            Command::End => {
                self.tx
                    .send(PacketC::ServerMessage {
                        message: trm!(
                            "s.state.game_aborted",
                            s = self
                                .game
                                .players
                                .get(&player)
                                .ok_or(tre!("s.error.no_player"))?
                                .name
                                .clone()
                        ),
                        error: false,
                    })
                    .ok();
                self.load(
                    self.index
                        .generate("lobby")
                        .await
                        .map_err(|e| TrError::Plain(e.to_string()))?,
                    None,
                );
            }
            Command::Reload => {
                if self.count_chefs() > 1 {
                    return Err(tre!("s.error.must_be_alone"));
                }
                self.load(
                    self.index
                        .generate(&self.game.data.current_map)
                        .await
                        .map_err(|e| TrError::Plain(e.to_string()))?,
                    None,
                );
            }
            Command::ReloadIndex => {
                self.index
                    .reload()
                    .await
                    .map_err(|e| TrError::Plain(e.to_string()))?;
            }
            Command::Book => replies.push(PacketC::Menu(Menu::Document(self.data.book.clone()))),
            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,
                }
                .map_err(|e| TrError::Plain(e.to_string()))?;
                replies.push(PacketC::ServerMessage {
                    message: Message::Text(source),
                    error: false,
                });
            }
            Command::List => replies.push(PacketC::ServerMessage {
                message: Message::Text(format!(
                    "Maps: {:?}\nRecipes: {:?}",
                    self.index.maps.keys().collect::>(),
                    self.index.recipes
                )),
                error: false,
            }),
            Command::Effect { name } => {
                self.tx.send(PacketC::Effect { name, player }).ok();
            }
            Command::Item { name } => {
                let item = self
                    .game
                    .data
                    .get_item_by_name(&name)
                    .ok_or(tre!("s.error.item_not_found", s = name))?;
                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(tre!("s.error.algo_not_found", s = algo))?;
                let algo = cons();
                self.entities.push(Box::new(BotDriver::new(
                    format!("{}-bot", name.unwrap_or((*aname).to_owned())),
                    51,
                    PlayerClass::Bot,
                    algo,
                )));
            }
            Command::Top { map, text } => {
                let mapname = map.as_ref().unwrap_or(&self.game.data.current_map);
                if let Some(board) = self.scoreboard.get(mapname) {
                    if text {
                        let mut o = format!("Scoreboard for {}:\n", mapname);
                        for (i, entry) in board.best.iter().take(5).enumerate() {
                            writeln!(
                                o,
                                "  {i}. {} points by {}",
                                entry.score.points,
                                entry.players.clone().join(", ")
                            )
                            .unwrap();
                        }
                        replies.push(PacketC::ServerMessage {
                            message: Message::Text(o),
                            error: false,
                        });
                    } else {
                        replies.push(PacketC::Menu(Menu::Document(DocumentElement::Document {
                            es: vec![DocumentElement::Page {
                                es: vec![
                                    DocumentElement::Par {
                                        es: vec![DocumentElement::Text {
                                            s: Message::Translation {
                                                id: "c.menu.scoreboard".to_string(),
                                                params: vec![],
                                            },
                                            size: 30.,
                                            bold: false,
                                            color: None,
                                            font: None,
                                        }],
                                    },
                                    DocumentElement::List {
                                        es: board
                                            .best
                                            .iter()
                                            .take(5)
                                            .enumerate()
                                            .map(|(place, entry)| DocumentElement::Text {
                                                s: trm!(
                                                    "c.menu.scoreboard.entry",
                                                    s = place.to_string(),
                                                    s = entry.score.points.to_string(),
                                                    s = entry.players.clone().join(", ")
                                                ),
                                                size: 15.,
                                                bold: false,
                                                color: None,
                                                font: None,
                                            })
                                            .collect(),
                                    },
                                ],
                                background: None,
                            }],
                        })));
                    }
                }
            }
            Command::Info { map } => {
                let mapname = map.as_ref().unwrap_or(&self.game.data.current_map);
                let info = self
                    .index
                    .maps
                    .get(mapname)
                    .ok_or(tre!("s.error.no_info"))?;
                replies.push(PacketC::ServerMessage {
                    message: Message::Text(format!(
                        "{:?}\nRecommended player count: {}\nDifficulty: {}",
                        info.name, info.difficulty, info.players
                    )),
                    error: false,
                });
            }
            Command::StartTutorial { item } => {
                let item = self
                    .game
                    .data
                    .get_item_by_name(&item)
                    .ok_or(tre!("s.error.item_not_found", s = item))?;
                #[cfg(not(test))] // TODO rust-analyser does not undestand trait upcasting
                if self.entities.iter().any(|e| {
                    ::downcast_ref::(e.as_ref())
                        .map_or(false, |t| t.player == player)
                }) {
                    return Err(tre!("s.error.tutorial_already_running"));
                }
                self.entities.push(Box::new(Tutorial::new(player, item)));
            }
            Command::EndTutorial => {
                #[cfg(not(test))] // TODO rust-analyser does not undestand trait upcasting
                if let Some(tutorial) = self
                    .entities
                    .iter_mut()
                    .find_map(|e| ::downcast_mut::(e.as_mut()))
                {
                    tutorial.ended = true;
                } else {
                    return Err(tre!("s.error.tutorial_no_running"));
                }
            }
            Command::TranslateMessage {
                message_id,
                arguments,
            } => {
                self.packet_out.push_back(PacketC::Communicate {
                    player,
                    message: Some(Message::Translation {
                        id: message_id,
                        params: arguments.into_iter().map(Message::Text).collect(),
                    }),
                    timeout: None,
                });
            }
        }
        Ok(())
    }
}