using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Concilium.Helpers;

namespace Concilium.Bot
{
    public class Resistance
    {
        private static class Strings
        {
            public static string Welcome = "A new The Resistance game was started by {0}!";
            public static string TooFewPlayers = "There are still too few players. At least 5 players are required.";
            public static string UserJoined = "{0} joined The Resistance game!";
            public static string UserCantJoin = "I'm sorry, {0}. You can't join the current The Resistance game, since it already started.";
            public static string UserDuplicate = "You already joined The Resistance game!";
            public static string GameBegins = "All necessary players joined! Let the game begin!";
            public static string HallOfFame = "Following players participated: ";
            public static string AgreeDisagreeNotice = "Every player must now " + (char)2 + ".resist agree" + (char)2 + " or " + (char)2 + ".resist disagree" + (char)2 + " with the team.";
            public static string ExecuteSabotageNotice = "You must now " + (char)2 + ".resist execute" + (char)2 + " or " + (char)2 + ".resist sabotage" + (char)2 + " the mission.";
            public static string ExecuteNotice = "You must now " + (char)2 + ".resist execute" + (char)2 + " the mission.";
            public static string PlayingSpy = "You're playing an imperial spy. Your job is to sabotage as many missions as possible, without revealing your identity to the resistance! The infiltrating spies are: {0}";
            public static string PlayingResistance = "You're playing in the resistance. Your job is to accomplish as many missions as possible.";
            public static string NewLeader = "A new leader has been determined: ";
            public static string LeaderOrder = "The order for determining leaders is as follows: ";
            public static string BeginGatheringTeam = "The leader must now choose the team members (attempt #{0}). {1} players must be in the team.";
            public static string BeginVotingTeam = "The proposed team consists of: {0}. Please vote now if you agree or disagree with that team.";
            public static string BeginMission = "The team will now execute the mission. If there are spies the mission might fail. Required spy sabotages for mission failure: {0}";
            public static string SpiesWin = "The spies have managed to sabotage 3 missions and won the game!";
            public static string ResistanceWins = "The resistance has managed to execute 3 missions and wins the game!";
            public static string ShowVotes = "All players voted, these are the results: {0}";
            public static string VoteFailed = "No majority could be found, a new team will be selected from a new leader.";
            public static string LastTry = "This is the last attempt for finding a team. The proposal of the leader will be automatically accepted.";
            public static string MissionFailed = "The mission was sabotaged and failed, because {0} player(s) sabotaged the mission. Score: Spies({1}), Resistance({2})";
            public static string MissionAccomplishedClean = "The mission was accomplished successfully without any sabotage. Score: Spies({0}), Resistance({1})";
            public static string MissionAccomplishedBarely = "The mission was accomplished successfully, although {0} player(s) committed sabotage. Score: Spies({1}), Resistance({2})";
            public static string MissionData1 = "There are 5 missions, each mission requires a team with a certain amount of people, and requires a certain amount of sabotage acts to fail.";
            public static string MissionData2 = "Team size/sabotages: ({0}/{1}), ({2}/{3}), ({4}/{5}), ({6}/{7}), ({8}/{9}).";
        }

        public enum State
        {
            Lobby,
            GatheringTeam,
            VotingTeam,
            Mission
        };

        public enum Role
        {
            Unassigned,
            Spy,
            Resistance
        };

        public delegate void DummyHandler();
        public event DummyHandler GameOver;
        private void SendGameOver()
        {
            if (GameOver != null)
                GameOver();
        }
        public event DummyHandler GatheringBegins;
        private void SendGatheringBegins()
        {
            if (GatheringBegins != null)
                GatheringBegins();
        }
        public event DummyHandler MissionBegins;
        private void SendMissionBegins()
        {
            if (MissionBegins != null)
                MissionBegins();
        }

        public delegate void LobbyNoticeHandler(string notice);
        public event LobbyNoticeHandler OnLobbyNotice;
        private void SendLobbyNotice(string notice)
        {
            if (OnLobbyNotice != null)
                OnLobbyNotice(notice);
        }

        public delegate void UsersNoticeHandler(IEnumerable<string> users, string notice);
        public event UsersNoticeHandler OnUsersNotice;
        private void SendUsersNotice(IEnumerable<string> users, string notice)
        {
            if (OnUsersNotice != null && users.Count() > 0)
                OnUsersNotice(users, notice);
        }

        public Resistance()
        {
            this.CurrentState = State.Lobby;
        }

        public void Init(string nickName)
        {
            this.GameCreator = nickName;
            this.Leader = null;
            this.NumPlayers = -1;
            this.CurrentMission = 0;
            this.ScoreSpies = 0;
            this.ScoreResistance = 0;
            this.TeamTries = 0;
            SendLobbyNotice(string.Format(Strings.Welcome, this.GameCreator));
        }

        public bool StartGame(string nickName)
        {
            if (CurrentState != State.Lobby)
            {
                return false;
            }

            if (GameCreator != nickName)
            {
                return false;
            }

            if (allPlayers.Count < 5)
            {
                SendLobbyNotice(string.Format(Strings.TooFewPlayers, nickName));
                return false;
            }

            this.NumPlayers = allPlayers.Count;
            CheckState();
            return true;
        }

        public bool JoinPlayer(string nickName)
        {
            if (CurrentState != State.Lobby)
            {
                SendUsersNotice(new List<string> { nickName }, string.Format(Strings.UserCantJoin, nickName));
                return false;
            }

            if (allPlayers.ContainsKey(nickName))
            {
                SendUsersNotice(new List<string> { nickName }, Strings.UserDuplicate);
                return false;
            }

            allPlayers[nickName] = Role.Unassigned;
            SendLobbyNotice(string.Format(Strings.UserJoined, nickName));
            CheckState();
            return true;
        }

        private int GetSpyCount()
        {
            switch (NumPlayers)
            {
                case 5: return 2;
                case 6: return 2;
                case 7: return 3;
                case 8: return 3;
                case 9: return 3;
                case 10: return 4;
            }

            throw new InvalidOperationException("Only 5 to 10 players supported");
        }

        private int GetTeamSize()
        {
            switch (NumPlayers)
            {
                case 5: return (new int[] { 2, 3, 2, 3, 3 })[CurrentMission - 1];
                case 6: return (new int[] { 2, 3, 4, 3, 4 })[CurrentMission - 1];
                case 7: return (new int[] { 2, 3, 3, 4, 4 })[CurrentMission - 1];
                case 8: return (new int[] { 3, 4, 4, 5, 5 })[CurrentMission - 1];
                case 9: return (new int[] { 3, 4, 4, 5, 5 })[CurrentMission - 1];
                case 10: return (new int[] { 3, 4, 4, 5, 5 })[CurrentMission - 1];
            }

            throw new InvalidOperationException("Only 5 to 10 players supported");
        }

        private int GetNumSabotages()
        {
            if (CurrentMission == 4 && NumPlayers >= 7)
                return 2;

            return 1;
        }

        public void CheckState()
        {
            if (string.IsNullOrEmpty(GameCreator))
                return;

            switch (CurrentState)
            {
                case State.Lobby:
                    if (allPlayers.Count == NumPlayers)
                    {
                        SendLobbyNotice(Strings.GameBegins);

                        // Assign roles
                        var players = new List<string>(allPlayers.Keys);
                        players.Shuffle();
                        var spies = players.Take(GetSpyCount()).ToList();
                        var resistants = players.Skip(GetSpyCount()).ToList();

                        foreach (var player in resistants)
                        {
                            allPlayers[player] = Role.Resistance;
                        }

                        foreach (var player in spies)
                        {
                            allPlayers[player] = Role.Spy;
                        }

                        players.Shuffle();
                        playerOrder.AddRange(players);
                        SendLobbyNotice(Strings.LeaderOrder + string.Join(", ", playerOrder));
                        SetLeader(playerOrder[0]);

                        SendUsersNotice(resistants, Strings.PlayingResistance);
                        SendUsersNotice(spies, string.Format(Strings.PlayingSpy, string.Join(", ", spies)));
                        GenerateHallOfFame();
                        SendMissionData();
                        BeginGatheringTeam();
                    }
                    break;
                case State.GatheringTeam:
                    if (TeamMembers.Count == GetTeamSize())
                    {
                        if (TeamTries >= 4)
                            BeginMission();
                        else
                            BeginVotingTeam();
                    }
                    break;
                case State.VotingTeam:
                    if (playerAgreesWithTeam.Count == NumPlayers)
                    {
                        SendLobbyNotice(string.Format(Strings.ShowVotes, string.Join(", ", playerAgreesWithTeam.Select(p => (p.Value ? "+" : "-") + p.Key))));

                        var agrees = 0;
                        var disagrees = 0;
                        foreach (var vote in playerAgreesWithTeam)
                        {
                            if (vote.Value)
                                ++agrees;
                            else
                                ++disagrees;
                        }

                        if (agrees > disagrees)
                        {
                            TeamTries = 0;
                            BeginMission();
                        }
                        else
                        {
                            SendLobbyNotice(Strings.VoteFailed);
                            RotateLeader();
                            ++TeamTries;
                            --CurrentMission;
                            BeginGatheringTeam();
                        }
                    }
                    break;
                case State.Mission:
                    if (playerSabotagesMission.Count == GetTeamSize())
                    {
                        var sabotages = 0;
                        foreach (var vote in playerAgreesWithTeam)
                        {
                            if (vote.Value)
                                ++sabotages;
                        }

                        var gameOver = false;
                        if (sabotages >= GetNumSabotages())
                        {
                            ++ScoreSpies;
                            SendLobbyNotice(string.Format(Strings.MissionFailed, sabotages, ScoreSpies, ScoreResistance));
                            if (ScoreSpies >= 3)
                            {
                                SendLobbyNotice(Strings.SpiesWin);
                                SendGameOver();
                                gameOver = true;
                            }
                        }
                        else
                        {
                            ++ScoreResistance;
                            if (sabotages == 0)
                                SendLobbyNotice(string.Format(Strings.MissionAccomplishedClean, ScoreSpies, ScoreResistance));
                            else
                                SendLobbyNotice(string.Format(Strings.MissionAccomplishedBarely, sabotages, ScoreSpies, ScoreResistance));
                            if (ScoreResistance >= 3)
                            {
                                SendLobbyNotice(Strings.ResistanceWins);
                                SendGameOver();
                                gameOver = true;
                            }
                        }

                        if (!gameOver)
                        {
                            RotateLeader();
                            BeginGatheringTeam();
                        }
                    }
                    break;
                default:
                    throw new NotImplementedException("BUG! Implementation incomplete, an internal state was not handled");
            }
        }

        public void SendMissionData()
        {
            SendLobbyNotice(Strings.MissionData1);
            SendLobbyNotice(Strings.MissionData2);
        }

        private void RotateLeader()
        {
            var currentIndex = playerOrder.IndexOf(Leader);
            var newIndex = currentIndex + 1;
            if (newIndex >= playerOrder.Count)
                newIndex = 0;
            SetLeader(playerOrder[newIndex]);
        }

        private void SetLeader(string player)
        {
            Leader = player;
            SendLobbyNotice(Strings.NewLeader + player);
        }

        private void BeginGatheringTeam()
        {
            if (CurrentState == State.GatheringTeam)
                throw new InvalidOperationException("Can't begin gathering team right now");

            ++CurrentMission;
            CurrentState = State.GatheringTeam;
            TeamMembers.Clear();
            SendLobbyNotice(string.Format(Strings.BeginGatheringTeam, TeamTries + 1, GetTeamSize()));
            if (TeamTries >= 4)
                SendLobbyNotice(string.Format(Strings.LastTry, GetTeamSize()));
        }

        private void BeginVotingTeam()
        {
            if (CurrentState != State.GatheringTeam)
                throw new InvalidOperationException("Can't begin voting team right now");

            CurrentState = State.VotingTeam;
            playerAgreesWithTeam.Clear();
            SendLobbyNotice(string.Format(Strings.BeginVotingTeam, string.Join(", ", TeamMembers)));
            SendLobbyNotice(Strings.AgreeDisagreeNotice);
        }

        private void BeginMission()
        {
            if (CurrentState != State.VotingTeam && CurrentState != State.GatheringTeam)
                throw new InvalidOperationException("Can't begin the mission right now");

            TeamTries = 0;
            CurrentState = State.Mission;
            playerSabotagesMission.Clear();
            SendLobbyNotice(string.Format(Strings.BeginMission, GetNumSabotages()));
            SendUsersNotice(ResistancePlayers(), Strings.ExecuteNotice);
            SendUsersNotice(SpyPlayers(), Strings.ExecuteSabotageNotice);
        }

        public void SetTeamMembers(string nick, IEnumerable<string> members)
        {
            if (nick != Leader)
            {
                SendUsersNotice(new string[] { nick }, string.Format("You're not the team leader, so you can't choose team members! {0} is the team leader.", Leader));
                return;
            }

            if (CurrentState != State.GatheringTeam)
            {
                SendUsersNotice(new string[] { nick }, "You cannot gather a team right now.");
                return;
            }

            if (members.Count() != GetTeamSize())
            {
                SendUsersNotice(new string[] { nick }, string.Format("You need to specify exactly {0} distinct members!", GetTeamSize()));
                return;
            }

            foreach (var member in members)
            {
                if (!allPlayers.ContainsKey(member))
                {
                    SendUsersNotice(new string[] { nick }, "There's no such player: " + member);
                    return;
                }
            }

            TeamMembers.Clear();
            TeamMembers.AddRange(members);
            CheckState();
        }

        public void AgreeTeam(string nick)
        {
            if (CurrentState != State.VotingTeam)
            {
                SendUsersNotice(new string[] { nick }, "You cannot agree for a team right now.");
                return;
            }

            if (playerAgreesWithTeam.ContainsKey(nick))
            {
                SendUsersNotice(new string[] { nick }, "You have already decided for this team.");
                return;
            }

            SendUsersNotice(new string[] { nick }, "You have agreed with the proposed team. This decision can't be undone.");

            playerAgreesWithTeam[nick] = true;
            CheckState();
        }

        public void DisagreeTeam(string nick)
        {
            if (CurrentState != State.VotingTeam)
            {
                SendUsersNotice(new string[] { nick }, "You cannot disagree for a team right now.");
                return;
            }

            if (playerAgreesWithTeam.ContainsKey(nick))
            {
                SendUsersNotice(new string[] { nick }, "You have already decided for this team.");
                return;
            }

            SendUsersNotice(new string[] { nick }, "You have disagreed with the proposed team. This decision can't be undone.");

            playerAgreesWithTeam[nick] = false;
            CheckState();
        }

        public void ExecuteMission(string nick)
        {
            if (CurrentState != State.Mission)
            {
                SendUsersNotice(new string[] { nick }, "You cannot execute the mission right now.");
                return;
            }

            if (playerSabotagesMission.ContainsKey(nick))
            {
                SendUsersNotice(new string[] { nick }, "You have already decided for this mission.");
                return;
            }

            if (!TeamMembers.Contains(nick))
            {
                SendUsersNotice(new string[] { nick }, "You can't decide for this mission since you're not on the team!");
                return;
            }

            playerSabotagesMission[nick] = false;
            CheckState();
        }

        public void SabotageMission(string nick)
        {
            if (CurrentState != State.Mission)
            {
                SendUsersNotice(new string[] { nick }, "You cannot sabotage the mission right now.");
                return;
            }

            if (allPlayers[nick] == Role.Resistance)
            {
                SendUsersNotice(new string[] { nick }, "You cannot sabotage the mission as a resistance member!.");
                return;
            }

            if (playerSabotagesMission.ContainsKey(nick))
            {
                SendUsersNotice(new string[] { nick }, "You have already decided for this mission.");
                return;
            }

            if (!TeamMembers.Contains(nick))
            {
                SendUsersNotice(new string[] { nick }, "You can't decide for this mission since you're not on the team!");
                return;
            }

            playerSabotagesMission[nick] = true;
            CheckState();
        }

        private void GenerateHallOfFame()
        {
            hallOfFame = Strings.HallOfFame + string.Join(", ", allPlayers.Select(p => p.Key + " (" + p.Value.ToString() + ")"));
        }

        public string GetStatus()
        {
            if (CurrentState == State.Lobby)
            {
                var message = "A game was recently created by " + GameCreator + ". Waiting for players to join. Following players already joined: ";
                var existing = string.Join(", ", GetAllPlayers());
                return message + existing;
            }
            else if (CurrentState == State.GatheringTeam)
            {
                var message = "The leader, {0}, is gathering the team. A total of {1} team members are needed for mission #{2}. Score: Spies({3}), Resistance({4})";
                return string.Format(message, Leader, GetTeamSize(), CurrentMission, ScoreSpies, ScoreResistance);
            }
            else if (CurrentState == State.VotingTeam)
            {
                var message = "The leader is {0}. Waiting for all players to vote for the mission team ({1}). Following players haven't voted yet: {2}. Score: Spies({3}), Resistance({4})";
                var missing = string.Join(", ", allPlayers.Where(p => !this.playerAgreesWithTeam.Keys.Contains(p.Key)).Select(p => p.Key));
                var team = string.Join(", ", TeamMembers);
                return string.Format(message, Leader, team, missing, ScoreSpies, ScoreResistance);
            }
            else if (CurrentState == State.Mission)
            {
                var message = "The leader is {0}. The team ({1}) is executing the mission. Each team member has to decide if the mission is executed or sabotaged. Following players haven't decided yet: {2}. Score: Spies({3}), Resistance({4})";
                var missing = string.Join(", ", TeamMembers.Where(p => !this.playerSabotagesMission.Keys.Contains(p)));
                var team = string.Join(", ", TeamMembers);
                return string.Format(message, Leader, team, missing, ScoreSpies, ScoreResistance);
            }

            return "";
        }

        public string GameCreator;
        public string Leader;
        public int CurrentMission;
        public int ScoreSpies;
        public int ScoreResistance;
        public int NumPlayers;
        public int TeamTries;
        public State CurrentState;
        public Dictionary<string, Role> allPlayers = new Dictionary<string, Role>();
        public Dictionary<string, bool> playerAgreesWithTeam = new Dictionary<string, bool>();
        public Dictionary<string, bool> playerSabotagesMission = new Dictionary<string, bool>();
        public List<string> TeamMembers = new List<string>();
        public List<string> playerOrder = new List<string>();
        public string hallOfFame;
        public IEnumerable<string> GetAllPlayers() { return allPlayers.Keys; }
        public IEnumerable<string> ResistancePlayers() { return allPlayers.Where(p => p.Value == Role.Resistance).Select(p => p.Key); }
        public IEnumerable<string> SpyPlayers() { return allPlayers.Where(p => p.Value == Role.Spy).Select(p => p.Key); }
    }
}
