use crate::game::{Command, Game, Reply};
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
/// Rock, Paper, Scissors.

/// A match. Considering to move this to Game.
struct Match {
    state: HashMap<String, Option<Play>>,
}

impl Match {
    fn new(h: &HashSet<String>) -> Self {
        let mut n = HashMap::new();
        for key in h {
            n.insert(key.clone(), None);
        }
        Match { state: n }
    }

    fn is_ready(&self) -> bool {
        self.state.values().all(|v| v.is_some())
    }

    fn opponent(&self, s: &String) -> String {
        self.state
            .keys()
            .find(|k| k != &s)
            .expect("Getting opponent, player name not in the set.")
            .to_string()
    }

    // Solves the game. MUST never call this method if there aren't two plays made.
    fn solve(&self) -> Reply {
        // Our reply.
        let mut r = Reply::new();
        // Players.
        let mut p = Vec::new();
        // And their choices:
        let mut c = Vec::new();
        for (i, j) in &self.state {
            p.push(i);
            c.push(
                j.as_ref()
                    .expect("Trying to solve a game, a player choice was None."),
            );
        }
        if c[0] == c[1] {
            r.push(format!(
                "@{} and @{} tie! You both chose {}. {}!",
                p[0],
                p[1],
                c[0],
                c[0].noise()
            ));
        } else if c[0] > c[1] {
            r.push(format!(
                "@{} is the winner! @{} lost. {} wins against {}. {}",
                p[0],
                p[1],
                c[0],
                c[1],
                c[0].noise()
            ));
        } else {
            r.push(format!(
                "@{} is the winner! @{} lost. {} wins against {}. {}",
                p[1],
                p[0],
                c[1],
                c[0],
                c[1].noise()
            ));
        }
        r
    }
}

/// Data structure of the game.
pub struct Rps {
    /// A HashSet with the players waiting to play as account strings.
    lobby: HashSet<String>,
    /// capacity determines  how many people a match contains.
    capacity: u8,
    // A vector of ongoing matches.
    matches: Vec<Match>,
    // HashSet indicating for each player which match they are in.
    players: HashMap<String, usize>,
}

impl Game for Rps {
    /// Creation of a new and empty Rps game structure.
    fn new() -> Self {
        Rps {
            lobby: HashSet::new(),
            capacity: 2,
            matches: Vec::new(),
            players: HashMap::new(),
        }
    }

    /// State machine that accepts a command, changes state and delivers replies if required.
    fn next(&mut self, m: &Command) -> Reply {
        let mut r = Reply::new();
        // The entire state depends on two factors: are we waiting to join a game, and are we playing a game?
        // It is possible for both to be false, and either one to be true.
        // If both are true, this is an error.
        let waiting = self.lobby.contains(&m.sender);
        let playing = self.players.contains_key(&m.sender);
        assert!(!(waiting & playing));
        // It is possible for a command to be: rps, a move in the game, or cancelrps.
        let joining = m.content == "rps";
        let quitting = m.content == "cancelrps";
        let choice = match m.content.as_str() {
            "rock" => Some(Play::Rock),
            "paper" => Some(Play::Paper),
            "scissors" => Some(Play::Scissors),
            _ => None,
        };
        // At most, one of these conditions can hold.
        assert!(!(joining && quitting));
        assert!(!(joining && choice.is_some()));
        assert!(!(quitting && choice.is_some()));
        // At this point we have all necessary information to match.
        /*         println!(
                    "{}, {}, {:?}, {}, {}.",
                    joining, quitting, choice, playing, waiting
                );
        */
        match (joining, quitting, choice, playing, waiting) {
            // We don't bother with the impossible cases that are already excluded by assertion.
            // Let's start with joining the game.
            // There are 3 cases we need care about:
            // We're joining, not playing and not waiting.
            (true, false, None, false, false) => {
                // This has two cases.
                // If there's nobody waiting.
                if self.capacity > 1 {
                    // Put sender in the lobby. Reduce capacity.
                    r.push(format!("@{} You've asked to join a game of Rock, Paper, Scissors. As soon as someone else wants to play, I'll send you a message so you can tell me your choice.", m.sender));
                    self.lobby.insert(m.sender.clone());
                    self.capacity -= 1;
                }
                // If Someone's waiting, start the game.
                else {
                    // Put player in the lobby. This avoids us making players mut.
                    self.lobby.insert(m.sender.clone());
                    // Get the playes from the lobby.
                    let players = self.lobby.clone();
                    // Create this match.
                    let this_match = Match::new(&players);
                    // Empty the lobby.
                    self.lobby = HashSet::new();
                    // Reset capacity.
                    self.capacity = 2;
                    // Add the match to the matches vector.
                    self.matches.push(this_match);
                    // Get the index.
                    let n = self.matches.len() - 1;
                    // Place the match index for each player.
                    for i in players {
                        self.players.insert(i, n);
                    }
                    // Prepare the replies.
                    // Make this reply quiet, so that the move is responded in DM.
                    r.quiet();
                    for i in self.matches[n].state.keys() {
                        r.push(format!(
                            "@{} Got a partner! Your opponent is {}

Tell me your choice: *rock*, *paper*, or *scissors*?",
                            i,
                            self.matches[n].opponent(i)
                        ));
                    }
                }
            }

            // We're trying to join, but already in the lobby.
            (true, false, None, false, true) => {
                r.push(format!(
                    "@{} You're already waiting for a game of Rock, Paper, Scissors. Be patient.",
                    m.sender
                ));
            }
            // We're trying to join, but already playing.
            (true, false, None, true, false) => {
                r.push(format!(
                    "@{} You're already playing a game against {}

You can cancel it with *cancelrps* if you're bored of waiting.",
                    m.sender,
                    self.matches[*self.players.get(&m.sender).expect("Trying to get the match index for a player who sent a join  command and is playing a game, but  is not in the set.")].opponent(&m.sender)
                ));
            }

            // Now we do the two quit cases: while waiting, and while playing.
            // While waiting, it only affects the player.
            (false, true, None, false, true) => {
                // Remove player from lobby.
                self.lobby.remove(&m.sender);
                // Reset capacity.
                self.capacity = 2;
                // Send message.
                r.push(format!(
"@{} You're no longer waiting for a partner. You may play again by sending *rps* any time.", m.sender));
            }
            // While playing, it affects both players.
            (false, true, None, true, false) => {
                // Get our match index.
                let n = self.players.get(&m.sender).expect("Trying to get the match of a player who sent a cancel command while playing a game, but is not in the set.");
                // Get our opponent.
                let o = self.matches[*n].opponent(&m.sender);
                // Remove the match.
                self.matches.remove(*n);
                // Remove both players from the player list.
                self.players.remove(&m.sender);
                self.players.remove(&o);
                // The simplest way to do a cancellation is to tell both players.
                // TODO: The non-cancelling player can go to the lobby in a future version.
                r.push(format!(
                    "@{} has cancelled the game with @{}

You're both welcome to play again any time. Use *rps* to start a new match.",
                    m.sender, o
                ));
            }
            // Now we deal with making a move.
            (false, false, Some(c), true, false) => {
                // Our name for later insertion.
                let name = m.sender.clone();
                // Get our match index.
                let n = self.players.get(&name).expect("Trying to get the match of a player who made a move while playing a game, but is not in the set.");
                // Get our opponent.
                let o = self.matches[*n].opponent(&name);
                // If we already played:
                if self.matches[*n].state.get(&name).expect("Trying to get the match of a player making a move while playing a game, but is not in the set.").is_some() {
                    // We can't play twice.
                    r.push(format!(
                        "@{} You already sent me your choice. You need to wait for {}

If you get bored, you can send me *cancelrps* to cancel the game.",
                        name, o
                    ));
                } else {
                    // Put our choice in the match.
                    self.matches[*n].state.insert(name, Some(c));
                    // Check if the match is done.
                    if self.matches[*n].is_ready() {
                        // We're ready. Solve the game.
                        r = self.matches[*n].solve();
                        // Clean up.
                        self.matches.remove(*n);
                        self.players.remove(&m.sender);
                        self.players.remove(&o);
                    } else {
                        // Game isn't over, just send the message.
                        r.push(format!(
                            "@{} Got your move. Let's see what your opponent does.",
                            m.sender
                        ));
                    }
                }
            }
            // And moves out of time.
            // When we're not in a game or waiting to play.
            (false, false, Some(_), false, false) => {
                r.push(format!("@{} You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", m.sender));
            }
            // When we're still waiting for a partner.
            (false, false, Some(_), false, true) => {
                r.push(format!("@{} You're still waiting for a partner. Be patient. If you want, you can sende me *cancelrps* to cancel the game.", m.sender));
            }

            _ => {} // __
        }

        r
    }
}

// Valid plays.
#[derive(PartialEq, Debug)]
enum Play {
    Rock,
    Paper,
    Scissors,
}

impl Play {
    // Silly sound effects.
    fn noise(&self) -> String {
        match self {
            Play::Rock => "CRUNCH!",
            Play::Scissors => "SNIP!",
            Play::Paper => "CRUMPLE!",
        }
        .to_string()
    }
}

impl PartialOrd for Play {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(match (self, other) {
            (x, y) if x == y => Ordering::Equal,
            (Play::Rock, Play::Paper) => Ordering::Less,
            (Play::Rock, Play::Scissors) => Ordering::Greater,
            (Play::Paper, Play::Rock) => Ordering::Greater,
            (Play::Paper, Play::Scissors) => Ordering::Less,
            (Play::Scissors, Play::Rock) => Ordering::Less,
            (Play::Scissors, Play::Paper) => Ordering::Greater,
            _ => Ordering::Equal,
        })
    }
}

impl std::fmt::Display for Play {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Play::Rock => write!(f, "Rock"),
            Play::Paper => write!(f, "Paper"),
            Play::Scissors => write!(f, "Scissors"),
        }
    }
}

/// Tests.
#[cfg(test)]
mod test {
    use crate::game::{Command, Game};
    use crate::rps::Rps;

    #[test]
    fn test_new() {
        let g = Rps::new();
        assert_eq!(g.capacity, 2);
        assert_eq!(g.lobby.len(), 0);
    }

    #[test]
    fn test_nonsense() {
        let c: Command = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "nonsense".to_string(),
        };
        let mut g = Rps::new();
        assert!(g.next(&c).0.is_empty());
    }

    #[test]
    fn test_first_join_game() {
        let mut g = Rps::new();
        let c = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rps".to_string(),
        };
        let r = g.next(&c);
        assert_eq!(r.0.len(), 1, "Incorrect number of replies: {}.", r.0.len());
        assert_eq!(r.0[0], "@modulux@node.isonomia.net You've asked to join a game of Rock, Paper, Scissors. As soon as someone else wants to play, I'll send you a message so you can tell me your choice.".to_string(), "Incorrect reply message: {}.", r.0[0]);
        assert_eq!(
            g.capacity, 1,
            "Capacity in lobby should be 1, is {}.",
            g.capacity
        );
    }

    #[test]
    fn test_join_game_twice() {
        let mut g = Rps::new();
        let c = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rps".to_string(),
        };
        g.next(&c);
        let r = g.next(&c);
        assert_eq!(r.0.len(), 1, "Incorrect number of replies: {:}.", r.0.len());
        assert_eq!(r.0[0], "@modulux@node.isonomia.net You're already waiting for a game of Rock, Paper, Scissors. Be patient.".to_string(), "Incorrect reply message: {}.", r.0[0]);
        assert_eq!(
            g.capacity, 1,
            "Capacity in lobby should be 1, is {}.",
            g.capacity
        );
    }

    #[test]
    fn test_join_game_complete() {
        let mut g = Rps::new();
        let c1 = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rps".to_string(),
        };
        g.next(&c1);
        let c2 = Command {
            sender: "modulux2@node.isonomia.net".to_string(),
            content: "rps".to_string(),
        };
        let r = g.next(&c2);
        assert_eq!(r.0.len(), 2, "Incorrect number of replies: {}.", r.0.len());
        assert!(r.0.contains(&"@modulux@node.isonomia.net Got a partner! Your opponent is modulux2@node.isonomia.net

Tell me your choice: *rock*, *paper*, or *scissors*?".to_string()), "Missing reply.");
        assert!(r.0.contains(&"@modulux2@node.isonomia.net Got a partner! Your opponent is modulux@node.isonomia.net

Tell me your choice: *rock*, *paper*, or *scissors*?".to_string()), "Missing reply.");
    }

    #[test]
    fn test_play_twice() {
        let mut g = Rps::new();
        let c1 = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rps".to_string(),
        };
        let c2 = Command {
            sender: "modulux2@node.isonomia.net".to_string(),
            content: "rps".to_string(),
        };
        let c3 = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rock".to_string(),
        };
        g.next(&c1);
        g.next(&c2);
        g.next(&c3);
        let r = g.next(&c3);
        assert_eq!(
            r.0.len(),
            1,
            "Incorrect number of replies. Reply: {:#?}.",
            r
        );

        assert_eq!(r.0[0], "@modulux@node.isonomia.net You already sent me your choice. You need to wait for modulux2@node.isonomia.net

If you get bored, you can send me *cancelrps* to cancel the game.", "Incorrect reply: {}.", r.0[0]);
    }

    #[test]
    fn test_play_too_early() {
        let mut g = Rps::new();
        let c = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rock".to_string(),
        };
        let r = g.next(&c);
        assert_eq!(r.0.len(), 1, "Incorrect number of replies in: {:?}", r);
        assert_eq!(r.0[0], "@modulux@node.isonomia.net You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", "{:?}", r);
    }

    #[test]
    fn twice_early_play() {
        let mut r;
        let mut g = Rps::new();
        let c1 = Command {
            sender: "modulux@node.isonomia.net".to_string(),
            content: "rock".to_string(),
        };
        let c2 = Command {
            sender: "modulux2@node.isonomia.net".to_string(),
            content: "rock".to_string(),
        };
        r = g.next(&c1);
        assert_eq!(r.0.len(), 1, "Incorrect number of replies in: {:?}", r);
        assert_eq!(r.0[0], "@modulux@node.isonomia.net You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", "{:?}", r);

        r = g.next(&c2);
        assert_eq!(r.0.len(), 1, "Incorrect number of replies in: {:?}", r);
        assert_eq!(r.0[0], "@modulux2@node.isonomia.net You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", "{:?}", r);
    }
    fn command(sender: &str, content: &str) -> Command {
        Command {
            sender: sender.to_string(),
            content: content.to_string(),
        }
    }

    #[test]
    fn test_join_full_then_cancel() {
        let r;
        let mut g = Rps::new();
        g.next(&command("modulux@node.isonomia.net", "rps"));
        g.next(&command("modulux2@node.isonomia.net", "rps"));
        r = g.next(&command("modulux@node.isonomia.net", "cancelrps"));
        assert_eq!(r.0.len(), 1, "Incorrect number of replies: {:?}", r);
        assert_eq!(
            r.0[0],
            "@modulux@node.isonomia.net has cancelled the game with @modulux2@node.isonomia.net

You're both welcome to play again any time. Use *rps* to start a new match.",
            "Incorrect response: {}",
            r.0[0]
        );
    }

    #[test]
    fn test_join_full_play_then_cancel() {
        let r;
        let mut g = Rps::new();
        g.next(&command("modulux@node.isonomia.net", "rps"));
        g.next(&command("modulux2@node.isonomia.net", "rps"));
        g.next(&command("modulux@node.isonomia.net", "rock"));
        r = g.next(&command("modulux@node.isonomia.net", "cancelrps"));
        assert_eq!(r.0.len(), 1, "Incorrect length. {:?}", r);
        assert_eq!(
            r.0[0],
            "@modulux@node.isonomia.net has cancelled the game with @modulux2@node.isonomia.net

You're both welcome to play again any time. Use *rps* to start a new match.",
            "Incorrect response. {}.",
            r.0[0]
        );
    }
}
