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

namespace Concilium.Bot
{
    public class Mafia
    {
        private static class Strings
        {
            public static string Welcome = "A new mafia game was started by {0}!";
            public static string TooFewPlayers = "There are still too few players. At least 5 players are required.";
            public static string PlayingMafioso = "Both you and {0} are playing as a mafiosos. Your job is to take over the city by ensuring that in the end the mafiosos outnumber the civilists. Kill one civilist every night.";
            public static string PlayingCivilist = "You're playing as a civilist. Your job is to try to discover the mafiosos infiltrating your city. Do a public voting each day on who should be killed. Kill all mafiosos and you win.";
            public static string PlayingDoctor = "You're playing as a doctor. Your job is to guard a civilist each night to prevent his/her death, should he become a target from the mafiosos infiltrating your city. You also count as a civilist, do a public voting each day on who should be killed. Kill all mafiosos and you win.";
            public static string PlayingDetective = "You're playing as a detective. Your job is to try each night to discover one of the mafiosos infiltrating your city. You also count as a civilist, do a public voting each day on who should be killed. Kill all mafiosos and you win.";
            public static string PlayingVigilante = "You're playing as a vigilante. Your job is to try to discover the mafiosos infiltrating your city. Do a public voting each day on who should be killed. At night you can kill any player at will. Kill all mafiosos and you win.";
            public static string HelpNightMafioso = "You're a mafioso and it's night. Discuss with the mafia and agree on one target. Vote for that target by querying " + (char)2 + ".mafia vote %nickName%" + (char)2 + " now.";
            public static string HelpNightCivilist = "You're civilist and it's night. Go to bed and relax.";
            public static string HelpNightDoctor = "You're a doctor and it's night. Your job is to guard and save a civilist now, do that by querying " + (char)2 + ".mafia save %nickName%" + (char)2 + " now.";
            public static string HelpNightDetective = "You're a detective and it's night. Your job is to detect whether a player is a mafioso. Do that by querying " + (char)2 + ".mafia detect %nickName%" + (char)2 + " now.";
            public static string HelpNightVigilante = "You're a vigilante and it's night. Your job is to kill all mafia players. Kill your suspect by querying " + (char)2 + ".mafia kill %nickName%" + (char)2 + " now.";
            public static string UserJoined = "{0} joined the mafia game!";
            public static string UserCantJoin = "I'm sorry, {0}. You can't join the current mafia game, since it already started.";
            public static string UserDuplicate = "You already joined the mafia game!";
            public static string GameBegins = "All necessary players joined! Let the game begin!";
            public static string DayBegins = "The sun comes out, it's day. Everybody wakes up.";
            public static string[] Deaths = new string[]
            {
                "This morning, the police found the body of {0} floating in the river. The chest had wounds from bullet impacts.",
                "After the neighbours alarmed the police, {0} was found dead at home. Experts believe that he was poisoned while consuming breakfast.",
                "The neighbourhood was scared this morning, when the car of {0} suddenly exploded. When the firemen arrived, the corpse was already carbonized.",
                "A citizen called the police this morning, indicating that he found the head of {0}, cut off, in his trash bin. There is no trace from the rest of the body."
            };
            public static string VoteStatus = "The citizens currently vote for killing: ";
            public static string DeathRole = "Upon closer inspection of the crime scene, the police could discover from the surrounding belongings that the victim was a " + (char)2 + "{0}" + (char)2 + ".";
            public static string LynchRole = "Upon closer inspection of the dead body, the citizens discover from the belongings that the victim was a " + (char)2 + "{0}" + (char)2 + ".";
            public static string NoDeathsOvernight = "This night was calm, that's a relief for the city. No player died.";
            public static string DayPreparation = "Each player must now vote who they think is a mafioso and should be killed. Just write via query " + (char)2 + ".mafia vote %nickName%" + (char)2 + " to vote.";
            public static string CantVote = "You can't vote at this time.";
            public static string CantUnvote = "You can't unvote at this time.";
            public static string InvalidVote = "I don't know who you mean. Please choose one from: ";
            public static string SelfVote = "You can't vote for youself!";
            public static string FirstVote = "{0} thinks that {1} should be killed.";
            public static string Unvote = "{0} is not so sure anymore and removes his/her vote.";
            public static string NextVote = "{0} changed his/her mind and proposes {1} to be killed.";
            public static string DayEnds = "As the day reaches its end and the sun sets, all players assemble in the execution place.";
            public static string LynchPlayer = "Agreed upon by public voting, {0} is pushed forward, and a rope is tied around the neck. Finally what remains is the lifeless look of {0}'s half-opened eyes.";
            public static string NightBegins = "The stars begin to shine, the city is quiet. It is now nighttime. All mafiosos will now meet in secret and agree on their victim.";
            public static string DetectiveIntro = "The detective might now be able to catch a mafioso on its way to their meeting place. Just write in a query " + (char)2 + ".mafia detect %nickName%" + (char)2 + " to check.";
            public static string DetectiveNotNight = "It's not night yet. You can only operate when the streets are dark.";
            public static string DetectiveAlreadyDone = "You've already done your job this night! You found out that {0} is{1} a mafioso.";
            public static string DetectiveFinished = "During your inspection this night you conclude that {0} is{1} a mafioso.";
            public static string DoctorIntro = "The doctor can now decide which player should be cured, for the case that this player will be a target of a mafioso attack. Just write in a query " + (char)2 + ".mafia save %nickName%" + (char)2 + " to save that player from a possible death.";
            public static string DoctorNotNight = "It's not night yet. The mafia only operates at night, and then it's where you should choose whom to guard.";
            public static string DoctorFinished = "OK, {0} will definitely survive this night.";
            public static string VigilanteNotNight = "It's not night yet. You should only do your killing at night.";
            public static string VigilanteFinished = "OK, you're about to kill {0} tonight.";
            public static string MafiosoIntro = "Each mafioso must now vote who they want to kill. All votes must target the same player. Just write in a query " + (char)2 + ".mafia vote %nickName%" + (char)2 + " to vote.";
            public static string NightEnds = "The stars start to fade. Slowly the sky turns into shades of blue.";
            public static string CivilistsWin = "All mafiosos were killed! Civilists win this game!";
            public static string MafiososWin = "The mafiosos managed to take over the city! They outnumber the citizens and therefore win this game!";
            public static string HallOfFame = "Following players participated: ";
        }

        public enum State
        {
            Lobby,
            Day,
            Night
        };

        public enum Role
        {
            Unassigned,
            Civilist,
            Mafioso,
            Detective,
            Doctor,
            Avenger,
            Vigilante
        };

        public delegate void DummyHandler();
        public event DummyHandler GameOver;
        private void SendGameOver()
        {
            if (GameOver != null)
                GameOver();
        }
        public event DummyHandler DayBegins;
        private void SendDayBegins()
        {
            if (DayBegins != null)
                DayBegins();
        }
        public event DummyHandler NightBegins;
        private void SendNightBegins()
        {
            if (NightBegins != null)
                NightBegins();
        }

        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 delegate void UserEventHandler(string nickName);
        public event UserEventHandler OnUserJoined;
        private void SendUserJoined(string nickName)
        {
            if (OnUserJoined != null)
                OnUserJoined(nickName);
        }
        public event UserEventHandler OnUserDied;
        private void SendUserDied(string nickName)
        {
            if (OnUserDied != null)
                OnUserDied(nickName);
        }

        public Mafia()
        {
            this.CurrentState = State.Lobby;
            this.FirstDay = true;
            this.NumPlayers = -1;
        }

        public void Init(string nickName)
        {
            this.GameCreator = nickName;
            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));
            OnUserJoined(nickName);
            CheckState();
            return true;
        }

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

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

                        var specialRoles = new List<Role>() { Role.Doctor, Role.Detective, Role.Vigilante };
                        specialRoles.Shuffle();

                        // Assign roles
                        var players = new List<string>(allPlayers.Keys);
                        players.Shuffle();
                        allPlayers[players[0]] = Role.Mafioso;
                        SendUsersNotice(new List<string>() { players[0] }, string.Format(Strings.PlayingMafioso, players[1]));
                        allPlayers[players[1]] = Role.Mafioso;
                        SendUsersNotice(new List<string>() { players[1] }, string.Format(Strings.PlayingMafioso, players[0]));
                        allPlayers[players[2]] = specialRoles.First(); specialRoles.RemoveAt(0);
                        allPlayers[players[3]] = specialRoles.First(); specialRoles.RemoveAt(0);
                        foreach (var remainingPlayer in allPlayers.Where(p => p.Value == Role.Unassigned).ToList())
                            allPlayers[remainingPlayer.Key] = Role.Civilist;
                        
                        SendUsersNotice(allPlayers.Where(p => p.Value == Role.Civilist).Select(p => p.Key), Strings.PlayingCivilist);
                        foreach (var playerEntry in allPlayers)
                        {
                            if (playerEntry.Value == Role.Detective)
                                SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.PlayingDetective);
                            else if (playerEntry.Value == Role.Doctor)
                                SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.PlayingDoctor);
                            else if (playerEntry.Value == Role.Vigilante)
                                SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.PlayingVigilante);
                        }
                        GenerateHallOfFame();
                        BeginDay();
                    }
                    break;
                case State.Day:
                    if (PlayerVotes.Count == allPlayers.Count)
                    {
                        SendLobbyNotice(Strings.DayEnds);
                        var dict = new Dictionary<string, int>();
                        foreach (var vote in PlayerVotes)
                        {
                            if (!dict.ContainsKey(vote.Value))
                                dict[vote.Value] = 0;

                            dict[vote.Value] = dict[vote.Value] + 1;
                        }

                        var maxVotes = dict.Max(d => d.Value);
                        var candidates = dict.Where(d => d.Value == maxVotes).Select(d => d.Key).ToArray();
                        var rand = new Random();
                        var candidate = candidates[rand.Next(candidates.Length)];
                        var role = allPlayers[candidate];
                        allPlayers.Remove(candidate);
                        OnUserDied(candidate);
                        SendLobbyNotice(string.Format(Strings.LynchPlayer, candidate));
                        SendLobbyNotice(string.Format(Strings.LynchRole, role.ToString()));
                        BeginNight();
                    }
                    break;
                case State.Night:
                    if (PlayerVotes.Count == GetMafiosos().Count() && PlayerVotes.All(v => v.Value == PlayerVotes.First().Value) && (GetDoctor() == null || DoctorSave != null) && (GetVigilante() == null || VigilanteKill != null) && (GetDetective() == null || DetectiveTarget != null))
                    {
                        MafiosoKill = PlayerVotes.First().Value;
                        SendLobbyNotice(Strings.NightEnds);
                        BeginDay();
                    }
                    break;
                default:
                    throw new NotImplementedException("BUG! Implementation incomplete, an internal state was not handled");
            }
        }

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

        private void BeginDay()
        {
            if (CurrentState == State.Day)
                throw new InvalidOperationException("BUG! Day can't begin when it's already day!");

            CurrentState = State.Day;
            SendLobbyNotice(Strings.DayBegins);
            PlayerVotes.Clear();

            if (!FirstDay)
            {
                var deaths = Strings.Deaths.ToList();
                deaths.Shuffle();
                var deathCount = 0;

                if (MafiosoKill != null && MafiosoKill != DoctorSave)
                {
                    var role = allPlayers[MafiosoKill];
                    allPlayers.Remove(MafiosoKill);
                    OnUserDied(MafiosoKill);
                    var death = deaths[deathCount++];
                    SendLobbyNotice(string.Format(death, MafiosoKill));
                    SendLobbyNotice(string.Format(Strings.DeathRole, role.ToString()));
                }

                if (VigilanteKill != null && VigilanteKill != DoctorSave)
                {
                    var role = allPlayers[VigilanteKill];
                    allPlayers.Remove(VigilanteKill);
                    OnUserDied(VigilanteKill);
                    var death = deaths[deathCount++];
                    SendLobbyNotice(string.Format(death, VigilanteKill));
                    SendLobbyNotice(string.Format(Strings.DeathRole, role.ToString()));
                }
                
                if (deathCount == 0)
                {
                    SendLobbyNotice(Strings.NoDeathsOvernight);
                }
            }
            else
            {
                FirstDay = false;
            }

            if (GetMafiosos().Count() == 0)
            {
                SendLobbyNotice(Strings.CivilistsWin);
                SendLobbyNotice(hallOfFame);
                SendGameOver();
            }
            else if (GetMafiosos().Count() > GetAllPlayers().Count() - GetMafiosos().Count())
            {
                SendLobbyNotice(Strings.MafiososWin);
                SendLobbyNotice(hallOfFame);
                SendGameOver();
            }
            else
            {
                SendLobbyNotice(Strings.DayPreparation);
                SendDayBegins();
            }
        }

        private void BeginNight()
        {
            if (CurrentState == State.Night)
                throw new InvalidOperationException("BUG! Night can't begin when it's already night!");

            foreach (var playerEntry in allPlayers)
            {
                switch (playerEntry.Value)
                {
                    case Role.Civilist: SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.HelpNightCivilist); break;
                    case Role.Mafioso: SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.HelpNightMafioso); break;
                    case Role.Doctor: SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.HelpNightDoctor); break;
                    case Role.Detective: SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.HelpNightDetective); break;
                    case Role.Vigilante: SendUsersNotice(new List<string>() { playerEntry.Key }, Strings.HelpNightVigilante); break;
                    default: break;
                }
            }


            if (GetMafiosos().Count() == 0)
            {
                SendLobbyNotice(Strings.CivilistsWin);
                SendLobbyNotice(hallOfFame);
                SendGameOver();
            }
            else if (GetMafiosos().Count() > GetAllPlayers().Count() - GetMafiosos().Count())
            {
                SendLobbyNotice(Strings.MafiososWin);
                SendLobbyNotice(hallOfFame);
                SendGameOver();
            }
            else
            {
                SendLobbyNotice(Strings.NightBegins);
                CurrentState = State.Night;
                PlayerVotes.Clear();
                DetectiveTarget = null;
                SendNightBegins();
            }
        }

        public bool Unvote(string nickName)
        {
            if (!allPlayers.ContainsKey(nickName))
            {
                return false;
            }

            if (CurrentState == State.Day)
            {
                if (PlayerVotes.ContainsKey(nickName))
                {
                    SendLobbyNotice(string.Format(Strings.Unvote, nickName));

                    PlayerVotes.Remove(nickName);
                    CheckState();
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else if (CurrentState == State.Night)
            {
                if (!GetMafiosos().Contains(nickName))
                    return false;

                var recipients = GetMafiosos().Where(p => p != nickName);
                if (PlayerVotes.ContainsKey(nickName))
                {
                    SendUsersNotice(recipients, string.Format(Strings.Unvote, nickName));
                    PlayerVotes.Remove(nickName);
                    CheckState();
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                SendUsersNotice(new List<string> { nickName }, Strings.CantUnvote);
                return false;
            }
        }

        public bool PlayerVote(string nickName, string vote)
        {
            if (!allPlayers.ContainsKey(vote))
            {
                var message = Strings.InvalidVote + string.Join(", ", GetAllPlayers().Where(p => p != nickName));
                SendUsersNotice(new List<string> { nickName }, message);
                return false;
            }

            if (!allPlayers.ContainsKey(nickName))
            {
                return false;
            }

            if (nickName == vote)
            {
                SendUsersNotice(new List<string> { nickName }, Strings.SelfVote);
                return false;
            }

            if (CurrentState == State.Day)
            {
                if (PlayerVotes.ContainsKey(nickName) && PlayerVotes[nickName] != vote)
                {
                    SendLobbyNotice(string.Format(Strings.NextVote, nickName, vote));
                }
                else
                {
                    SendLobbyNotice(string.Format(Strings.FirstVote, nickName, vote));
                }

                PlayerVotes[nickName] = vote;
                SendLobbyNotice(GetVoteStatus());
                CheckState();
                return true;
            }
            else if (CurrentState == State.Night)
            {
                if (!GetMafiosos().Contains(nickName))
                    return false;

                var recipients = GetMafiosos().Where(p => p != nickName);
                if (PlayerVotes.ContainsKey(nickName) && PlayerVotes[nickName] != vote)
                {
                    SendUsersNotice(recipients, string.Format(Strings.NextVote, nickName, vote));
                }
                else
                {
                    SendUsersNotice(recipients, string.Format(Strings.FirstVote, nickName, vote));
                }

                PlayerVotes[nickName] = vote;
                CheckState();
                return true;
            }
            else
            {
                SendUsersNotice(new List<string> { nickName }, Strings.CantVote);
                return false;
            }
        }

        public string GetVoteStatus()
        {
            if (CurrentState == State.Day)
            {
                var message = Strings.VoteStatus;
                message += string.Join(", ", PlayerVotes.Select(v => string.Format("{0} => {1}", v.Key, v.Value)));
                if (PlayerVotes.Count == 0)
                    message += "(none)";
                return message;
            }

            return "";
        }

        public bool DetectMafioso(string nickName, string query)
        {
            if (GetDetective() != nickName)
            {
                return false;
            }

            if (CurrentState != State.Night)
            {
                SendUsersNotice(new List<string> { nickName }, Strings.DetectiveNotNight);
                return false;
            }

            if (!allPlayers.ContainsKey(query))
            {
                var message = Strings.InvalidVote + string.Join(", ", GetAllPlayers().Where(p => p != nickName));
                SendUsersNotice(new List<string> { nickName }, message);
                return false;
            }

            var not = GetMafiosos().Any(s => s == query) ? "" : " not";
            if (DetectiveTarget != null)
            {
                SendUsersNotice(new List<string> { nickName }, string.Format(Strings.DetectiveAlreadyDone, DetectiveTarget, not));
                return false;
            }

            DetectiveTarget = query;
            SendUsersNotice(new List<string> { nickName }, string.Format(Strings.DetectiveFinished, DetectiveTarget, not));
            CheckState();
            return true;
        }

        public bool SaveCivilist(string nickName, string query)
        {
            if (GetDoctor() != nickName)
            {
                return false;
            }

            if (CurrentState != State.Night)
            {
                SendUsersNotice(new List<string> { nickName }, Strings.DoctorNotNight);
                return false;
            }

            if (!allPlayers.ContainsKey(query))
            {
                var message = Strings.InvalidVote + string.Join(", ", GetAllPlayers().Where(p => p != nickName));
                SendUsersNotice(new List<string> { nickName }, message);
                return false;
            }

            DoctorSave = query;
            SendUsersNotice(new List<string> { nickName }, string.Format(Strings.DoctorFinished, DoctorSave));
            CheckState();
            return true;
        }

        public bool VoteVigilanteKill(string nickName, string query)
        {
            if (GetVigilante() != nickName)
            {
                return false;
            }

            if (CurrentState != State.Night)
            {
                SendUsersNotice(new List<string> { nickName }, Strings.VigilanteNotNight);
                return false;
            }

            if (!allPlayers.ContainsKey(query))
            {
                var message = Strings.InvalidVote + string.Join(", ", GetAllPlayers().Where(p => p != nickName));
                SendUsersNotice(new List<string> { nickName }, message);
                return false;
            }

            VigilanteKill = query;
            SendUsersNotice(new List<string> { nickName }, string.Format(Strings.VigilanteFinished, VigilanteKill));
            CheckState();
            return true;
        }

        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.Day)
            {
                var message = "It's day. Waiting for all players to place their vote. Following players haven't voted: ";
                var missing = string.Join(", ", GetAllPlayers().Where(p => !PlayerVotes.Keys.Contains(p)));
                return message + missing + ". " + GetVoteStatus();
            }
            else if (CurrentState == State.Night)
            {
                var message = "It's currently night. Everybody is sleeping. (yeah right)";
                return message;
            }

            return "";
        }

        public string MafiosoKill;
        public string DoctorSave;
        public string VigilanteKill;
        public string DetectiveTarget;
        public string GameCreator;
        public bool FirstDay;
        public int NumPlayers;
        public State CurrentState;
        public Dictionary<string, Role> allPlayers = new Dictionary<string, Role>();
        public string hallOfFame;
        public IEnumerable<string> GetAllPlayers() { return allPlayers.Keys; }
        public string GetDoctor() { return allPlayers.FirstOrDefault(p => p.Value == Role.Doctor).Key; }
        public string GetVigilante() { return allPlayers.FirstOrDefault(p => p.Value == Role.Vigilante).Key; }
        public string GetDetective() { return allPlayers.FirstOrDefault(p => p.Value == Role.Detective).Key; }
        public IEnumerable<string> GetMafiosos() { return allPlayers.Where(p => p.Value == Role.Mafioso).Select(p => p.Key); }
        public IEnumerable<string> GetCivilists() { return allPlayers.Where(p => p.Value == Role.Civilist).Select(p => p.Key); }
        public Dictionary<string, string> PlayerVotes = new Dictionary<string, string>();
    }
}
