namespace P4
{

    // Création d'un enum pour les jetons
    public enum Token
    {
        None,
        FirstPlayer,
        SecondPlayer,
    }

    /// <summary>
    /// Classe programme c'est la classe qui est exécutée par le programme
    /// </summary>
    public class Program
    {
        // Déclaration des couleurs des joueurs
        public readonly ConsoleColor FIRSTPLAYERCOLOR = ConsoleColor.Red;
        public readonly ConsoleColor SECONDPLAYERCOLOR = ConsoleColor.Yellow;

        // Déclaration des couleurs de la console
        const ConsoleColor BACKGROUND_COLOR = ConsoleColor.Black;
        const ConsoleColor FOREGROUND_COLOR = ConsoleColor.White;

        /// <summary>
        /// Méthode pour changer les couleurs de la console
        /// </summary>
        /// <param name="backgroundColor">couleur de fond</param>
        /// <param name="foregroundColor">couleur du texte</param>
        public void SetColor(ConsoleColor backgroundColor = BACKGROUND_COLOR, ConsoleColor foregroundColor = FOREGROUND_COLOR)
        {
            Console.ForegroundColor = foregroundColor;
            Console.BackgroundColor = backgroundColor;
        }

        /// <summary>
        /// Méthode Main la méthode qui s'exécute au lancement du programme
        /// </summary>
        /// <param name="args">tableau des arguments données au programme</param>
        static void Main(string[] args)
        {
            // on définit les couleurs par défaut de la console
            new Program().SetColor();

            // on vide la console
            Console.Clear();

            // on dessine le titre
            new Draw().DrawTitle();

            // on enlève le curseur
            Console.CursorVisible = false;

            // on récupère la taille du plateau
            int[] boardSize = new Board().BoardSize();

            // on crée un tableau de jetons
            Token[,] board = new Token[boardSize[0], boardSize[1]];

            // on lance le jeu
            new Game().Start(board);

            // on demande si les joueurs veulent rejouer
            new Game().Replay();
        }
    }

    /// <summary>
    /// Classe Game qui contient toutes les méthodes pour jouer
    /// </summary>
    public class Game
    {
        // on crée des instances d'autres classes
        private Draw _draw = new Draw();
        private Program _program = new Program();

        /// <summary>
        /// Méthode pour demander si les joueurs veulent rejouer et pour relancer le jeu si c'est le cas
        /// </summary>
        public void Replay()
        {
            // on demande si l'utilisateur veut rejouer
            Console.WriteLine("Voulez-vous rejouer ? (o/n)");

            // on regarde quelle touche a été pressée
            switch (Console.ReadKey().Key)
            {
                case ConsoleKey.O:
                    // si c'est oui on relance le jeu
                    new Program().SetColor();
                    Console.Clear();
                    new Draw().DrawTitle();
                    int[] boardSize = new Board().BoardSize();
                    Token[,] board = new Token[boardSize[0], boardSize[1]];
                    new Game().Start(board);
                    new Game().Replay();
                    break;
                case ConsoleKey.N:
                    // si c'est non on quitte le programme
                    return;
                default:
                    // sinon on demande de réessayer
                    Console.WriteLine("Entrée invalide");
                    Replay();
                    break;

            }
        }

        /// <summary>
        /// Méthode pour lancer la boucle principale du jeu
        /// </summary>
        /// <param name="board">tableau de jetons en 2 dimensions</param>
        public void Start(Token[,] board)
        {

            // initialisation des variables
            int currentPosition = 0; // position actuelle
            Token currentPlayer = Token.FirstPlayer; // joueur actuel
            bool placed = false; // vérification s'il on peut placer un jeton

            // on vide la console
            Console.Clear();

            // on efface le curseur au cas où la taille de la fenêtre aurait changé
            Console.CursorVisible = false;

            // on dessine le titre l'espace de jeu le plateau et l'aide
            _draw.DrawTitle(_draw.leftSpacing);
            _draw.DrawPlayGroundBorder(board);
            _draw.DrawBorder(board);
            _draw.DrawHelp(board);

            // boucle principale du jeu
            while (!CheckWin(board))
            {
                // on vérifie s'il y a égalité
                if (CheckDraw(board))
                    break;

                // on dessine les jetons dans l'espace de jeu et le plateau
                _draw.DrawBoard(board);
                _draw.DrawPlayGround(board, currentPlayer, currentPosition);

                // on récupère la touche pressée
                ConsoleKey key = Console.ReadKey(true).Key;

                // on regarde si la touche est escape
                if (key == ConsoleKey.Escape)
                {
                    // s'il le joueur appuie sur escape il n'y a pas de gagnant donc on set le currentPlayer à None
                    currentPlayer = Token.None;
                    break;
                }

                // on regarde les actions des joueurs
                switch (key)
                {
                    case ConsoleKey.RightArrow:
                        // on incrémente la position actuelle si elle est inférieure à la taille du plateau
                        if (currentPosition < board.GetLength(0) - 1)
                            currentPosition++;
                        else
                            // on loop si on est à la fin du plateau
                            currentPosition = 0;
                        break;

                    case ConsoleKey.LeftArrow:
                        // on decremente la position actuelle si elle est supérieure à 0
                        if (currentPosition > 0)
                            currentPosition--;
                        else
                            // on loop si on est à le début du plateau
                            currentPosition = board.GetLength(0) - 1;
                        break;

                    case ConsoleKey.Enter:
                    case ConsoleKey.Spacebar:
                        // on place le jeton et on change de joueur
                        placed = PlaceToken(board, currentPlayer, currentPosition);
                        if (placed && currentPlayer == Token.FirstPlayer)
                            currentPlayer = Token.SecondPlayer;
                        else if (placed && currentPlayer == Token.SecondPlayer)
                            currentPlayer = Token.FirstPlayer;
                        break;
                }
            }

            // on dessine les derniers jetons
            _draw.DrawBoard(board);
            _draw.DrawPlayGround(board, currentPlayer, currentPosition);

            // on reset les couleurs
            _program.SetColor();

            // on place le curseur en bas du tableau avec le bon spacing
            Console.SetCursorPosition(0, board.GetLength(1) * 2 + _draw.topSpacing + 4);

            // on verifie s'il y a une égalité
            if (CheckDraw(board))
                Console.WriteLine("il y a eu une égalité");
            else if (currentPlayer == Token.FirstPlayer)
            {
                // si le joueur actuel est le joueur 1 cela signifie que le joueur 2 a gagné
                Console.Write("Le ");
                _program.SetColor(foregroundColor: _program.SECONDPLAYERCOLOR);
                Console.Write("joueur 2");
                _program.SetColor();
                Console.WriteLine(" a gagné");

            }
            else if (currentPlayer == Token.SecondPlayer)
            {
                // si le joueur actuel est le joueur 2 cela signifie que le joueur 1 a gagné
                Console.Write("Le ");
                _program.SetColor(foregroundColor: _program.FIRSTPLAYERCOLOR);
                Console.Write("joueur 1");
                _program.SetColor();
                Console.WriteLine(" a gagné");
            } 
            else if (currentPlayer == Token.None)
            {
                // si le joueur actuel est None cela signifie que la partie a été arrêtée
                Console.WriteLine("La partie a été arrêtée");
            }
            // on affiche le nombre de tours
            Console.WriteLine("La partie a duré " + Turn(board) + " tours");
        }

        /// <summary>
        /// Méthode pour compter le nombre de jetons sur le plateau
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        /// <returns>nombre de jetons sur le plateau</returns>
        int Turn(Token[,] board)
        {
            int count = 0; // compteur
            for (int y = 0; y < board.GetLength(1); y++)
            {
                for (int x = 0; x < board.GetLength(0); x++)
                {
                    if (board[x, y] != 0)
                    {
                        // ajoute 1 si le jeton n'est pas égal à 0
                        count++;
                    }
                }
            }
            return count;
        }

        /// <summary>
        /// Méthode pour vérifier s'il y a une égalité
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        /// <returns>return false t'en qu'il y a au moins 1 0 sur le plateau</returns>
        private bool CheckDraw(Token[,] board)
        {
            for (int y = 0; y < board.GetLength(1); y++)
            {
                for (int x = 0; x < board.GetLength(0); x++)
                {
                    if (board[x, y] == 0)
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        /// <summary>
        /// Méthode qui vérifie si un joueur a gagné
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        /// <returns>return true s'il y a une combinaison gagnante</returns>
        private bool CheckWin(Token[,] board)
        {
            // tableau des joueurs
            Token[] players = {Token.FirstPlayer, Token.SecondPlayer};
            // boucle qui alterne entre les joueurs 1 et 2
            foreach (var j in players)
            {
                // boucle qui fait toutes les colonnes
                for (int y = 0; y < board.GetLength(1); y++)
                {
                    // boucle qui fait toute la valeur des colonnes
                    for (int x = 0; x < board.GetLength(0); x++)
                    {
                        // on check les bordures horizontales
                        if (x + 3 < board.GetLength(0))
                        {
                            // on check les 4 prochains jetons dans le sens horizontal
                            if (board[x, y] == j &&
                                board[x + 1, y] == j &&
                                board[x + 2, y] == j &&
                                board[x + 3, y] == j)
                            {
                                // on retourne vrai si la condition est respectée
                                return true;
                            }
                        }
                        // on check les bordure verticales
                        if (y + 3 < board.GetLength(1))
                        {
                            // on check les 4 prochains jetons dans le sens vertical
                            if (board[x, y] == j &&
                                board[x, y + 1] == j &&
                                board[x, y + 2] == j &&
                                board[x, y + 3] == j)
                            {
                                // on retourne vrai si la condition est respectée
                                return true;
                            }
                        }
                        // on check les bordures horizontales et verticales
                        if (y + 3 < board.GetLength(1) && x + 3 < board.GetLength(0))
                        {
                            // on check les 4 prochains jetons dans le sens horizontal et vertical
                            if (board[x, y] == j &&
                                board[x + 1, y + 1] == j &&
                                board[x + 2, y + 2] == j &&
                                board[x + 3, y + 3] == j)
                            {
                                // on retourne vrai si la condition est respectée
                                return true;
                            }
                        }
                        // on check les bordures horizontales et verticales
                        if (y - 3 > 0 && x + 3 < board.GetLength(0))
                        {
                            // on check les 4 prochains jetons dans le sens horizontal et vertical
                            if (board[x, y] == j &&
                                board[x + 1, y - 1] == j &&
                                board[x + 2, y - 2] == j &&
                                board[x + 3, y - 3] == j)
                            {
                                // on retourne vrai si la condition est respectée
                                return true;
                            }
                        }
                    }
                }
            }

            // si rien n'a été trouvé, on retourne false
            return false;
        }

        /// <summary>
        /// Méthode qui place un jeton sur le plateau
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        /// <param name="currentPlayer">joueur actuel</param>
        /// <param name="currentPosition">position du joueur</param>
        /// <returns></returns>
        private bool PlaceToken(Token[,] board, Token currentPlayer, int currentPosition)
        {
            for (int i = board.GetLength(1) - 1; i >= 0; i--)
            {
                // si la case est déjà occupée on continue
                if (board[currentPosition, i] != Token.None) continue;

                // sinon on place le jeton
                board[currentPosition, i] = currentPlayer;

                // on revoie vrai s'il on a placé le jeton
                return true;
            }
            // on revoie faux s'il on n'a pas placé le jeton
            return false;
        }
    }
    public class Board
    {
        private Program _program = new Program();
        public int[] BoardSize()
        {
            int[] boardSize = { 0, 0 }; // initialisation de la taille
            string[] input = { "", "" }; // initialisation de l'entrée utilisateur
            // écriture des instructions en couleur grâce à la méthode WriteColor
            Console.Write("La taille doit être comprise entre ");
            _program.SetColor(foregroundColor: ConsoleColor.Red);
            Console.Write("5x6");
            _program.SetColor();
            Console.Write(" et ");
            _program.SetColor(foregroundColor: ConsoleColor.Red);
            Console.Write("13x16\n");
            _program.SetColor();
            Console.WriteLine("exemple: 6x7 (6 lignes et 7 colonnes)");
            Console.Write("taille: ");

            // récupération de l'entrée utilisateur dans une liste grâce à un split
            input = Console.ReadLine().Split('x');

            // on essaye de Parse en int les entrées, sinon on fait une récursion
            try
            {
                boardSize[0] = int.Parse(input[1]);
                boardSize[1] = int.Parse(input[0]);
            }
            catch
            {
                Console.WriteLine("La taille est invalide. réessayer");
                return BoardSize();
            }

            // on check si les entrées sont incorrectes si oui on fait une récursion
            if (boardSize[1] <= 5 || boardSize[1] >= 13 || boardSize[0] <= 6 || boardSize[0] >= 16)
            {
                Console.WriteLine("La taille est invalide. réessayer");
                return BoardSize();
            }

            // et si tous les checks sont passés, on retourne la taille
            Console.WriteLine("taille valide");
            return boardSize;
        }
    }

    /// <summary>
    /// Classe Draw qui contient toutes les méthodes pour render
    /// </summary>
    public class Draw
    {
        // on instancie la classe Program
        private Program _program = new Program();

        // on définit les espacements
        public int topSpacing = 4;
        public int leftSpacing = 10;

        /// <summary>
        /// Méthode DrawPlayGroundBorder qui dessine le terrain de jeu
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        public void DrawPlayGroundBorder(Token[,] board)
        {
            // on place le curseur en haut du terrain de jeu après le titre
            Console.SetCursorPosition(leftSpacing, topSpacing);
            // on dessine la première ligne du terrain de jeu
            Console.Write("╔═══");
            for (int x = 0; x < board.GetLength(0) - 1; x++)
            {
                Console.Write("╦═══");
            }
            Console.Write("╗");


            // on dessine les autres lignes du terrain de jeu
            for (int y = 0; y < board.GetLength(1); y++)
            {
                for (int x = 0; x < board.GetLength(0); x++)
                {
                    Console.SetCursorPosition(leftSpacing + x * 4, y * 2 + topSpacing + 1);
                    Console.Write("║");
                }
                Console.Write("   ║");
            }

            // on dessine la dernière ligne du terrain de jeu
            Console.SetCursorPosition(leftSpacing, topSpacing + 2);
            Console.Write("╚═══");
            for (int x = 0; x < board.GetLength(0) - 1; x++)
            {
                Console.Write("╩═══");
            }
            Console.Write("╝");
        }

        /// <summary>
        /// Méthode DrawPlayGround qui dessine les jetons dans le terrain de jeu
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        /// <param name="currentPlayer">joueur actuel</param>
        /// <param name="currentPosition">position du joueur actuel</param>
        public void DrawPlayGround(Token[,] board, Token currentPlayer, int currentPosition)
        {
            for (int x = 0; x < board.GetLength(0); x++)
            {
                // on place le curseur dans chaque case du terrain de jeu
                Console.SetCursorPosition(leftSpacing + x * 4+2, topSpacing + 1);

                // on dessine le jeton du joueur actuel si la position actuelle est égale à la position x sinon on ne dessine rien
                if (currentPosition == x) Console.Write(DrawToken(currentPlayer));
                else Console.Write(DrawToken(Token.None));
            }
        }

        /// <summary>
        /// Méthode DrawBorder qui dessine les bordures du plateau
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        public void DrawBorder(Token[,] board)
        {
            // déclaration de l'espacement du plateau
            int playgroundSpacing = topSpacing + 3;

            // on place le curseur en haut du plateau
            Console.SetCursorPosition(leftSpacing, playgroundSpacing);

            // on dessine la première ligne du plateau
            Console.Write("╔═══");
            for (int x = 0; x < board.GetLength(0) - 1; x++)
            {
                Console.Write("╦═══");
            }
            Console.Write("╗");

            // on dessine les autres lignes du plateau
            for (int y = 0; y < board.GetLength(1); y++)
            {
                for (int x = 0; x < board.GetLength(0); x++)
                {
                    Console.SetCursorPosition(leftSpacing + x * 4, y * 2 + playgroundSpacing + 1);
                    Console.Write("║");
                }
                Console.SetCursorPosition(leftSpacing + board.GetLength(0) * 4, y * 2 + playgroundSpacing + 1);
                Console.Write("║");
            }

            // on dessine les interligne du plateau
            for (int y = 0; y < board.GetLength(1) - 1; y++)
            {
                Console.SetCursorPosition(leftSpacing, y * 2 + playgroundSpacing + 2);
                Console.Write("╠═══");
                for (int x = 1; x < board.GetLength(0); x++)
                {
                    Console.SetCursorPosition(leftSpacing + x * 4, y * 2 + playgroundSpacing + 2);
                    Console.Write("╬═══");
                }
                Console.Write("╣");
            }

            // on dessine la dernière ligne du plateau
            Console.SetCursorPosition(leftSpacing, board.GetLength(1) * 2 + playgroundSpacing);
            Console.Write("╚═══");
            for (int x = 0; x < board.GetLength(0) - 1; x++)
            {
                Console.Write("╩═══");
            }
            Console.Write("╝");
        }

        /// <summary>
        /// Méthode DrawBoard qui dessine les jetons sur le plateau
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        public void DrawBoard(Token[,] board)
        {
            // déclaration de l'espacement du plateau
            int playgroundSpacing = topSpacing + 3;
            for (int y = 0; y < board.GetLength(1); y++)
                for (int x = 0; x < board.GetLength(0); x++)
                {
                    // on place le curseur dans chaque case du plateau
                    Console.SetCursorPosition(leftSpacing + x * 4+2, y*2 + playgroundSpacing + 1);
                    // on dessine le jeton qui correspond à la case
                    Console.Write("{0}", DrawToken(board[x, y]));
                }
        }

        /// <summary>
        /// Méthode DrawToken qui transforme un jeton en string
        /// </summary>
        /// <param name="token">jeton à transformer</param>
        /// <returns>jeton transformé</returns>
        public string DrawToken(Token token)
        {
            switch (token)
            {
                case Token.FirstPlayer:
                    // on set la couleur du premier joueur
                    _program.SetColor(foregroundColor: _program.FIRSTPLAYERCOLOR);
                    // on retourne le jeton
                    return "■";
                case Token.SecondPlayer:
                    // on set la couleur du deuxième joueur
                    _program.SetColor(foregroundColor: _program.SECONDPLAYERCOLOR);
                    // on retourne le jeton
                    return "■";
                case Token.None:
                    // on set la couleur par défaut
                    _program.SetColor(foregroundColor: ConsoleColor.DarkGray);
                    // on retourne le jeton
                    return "≡";
                default:
                    // revoye le rest au cas où
                    return default;
            }
        }

        /// <summary>
        /// Méthode DrawTitle qui dessine le titre du jeu
        /// </summary>
        /// <param name="spacing">espacement entre le bord et le titre</param>
        public void DrawTitle(int spacing = 0)
        {
            // on met le curseur à la bonne position avant d'écrire chaques lignes
            Console.SetCursorPosition(spacing, 0);
            Console.WriteLine("╔═══════════════════════════════════╗");
            Console.SetCursorPosition(spacing, 1);
            Console.WriteLine("║ Bienvenue dans le jeu Puissance 4 ║");
            Console.SetCursorPosition(spacing, 2);
            Console.WriteLine("║ Réalisé par Théophile Borboën     ║");
            Console.SetCursorPosition(spacing, 3);
            Console.WriteLine("╚═══════════════════════════════════╝");

        }

        /// <summary>
        /// Méthode DrawHelp qui dessine l'aide du jeu
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        public void DrawHelp(Token[,] board)
        {
            // on dessine l'aide relative au plateau et qu'avec des positions
            WriteHelp(board, 0, 0, "Mode d'utilisation");
            WriteHelp(board, 0, 1, "-------------------");
            WriteHelp(board, 4, 2, "Déplacement");
            WriteHelp(board, 18, 2, "Touches directionnelles");
            WriteHelp(board, 4, 3, "Tir");
            WriteHelp(board, 18, 3, "Spacebar ou Enter");
            WriteHelp(board, 4, 4, "Quitter");
            WriteHelp(board, 18, 4, "Escape");
            _program.SetColor(foregroundColor: _program.FIRSTPLAYERCOLOR);
            WriteHelp(board, 4, 6, "Joueur 1: ");
            _program.SetColor(_program.FIRSTPLAYERCOLOR);
            WriteHelp(board, 14, 6, " ");
            _program.SetColor(foregroundColor: _program.SECONDPLAYERCOLOR);
            WriteHelp(board, 18, 6, "Joueur 2: ");
            _program.SetColor(_program.SECONDPLAYERCOLOR);
            WriteHelp(board, 28, 6, " ");
        }

        /// <summary>
        /// Méthode WriteHelp qui écrit les phrases de l'aide
        /// </summary>
        /// <param name="board">tableau de jetons</param>
        /// <param name="x">position x</param>
        /// <param name="y">position y</param>
        /// <param name="text">text à écrire à cette position</param>
        private void WriteHelp(Token[,] board, int x, int y, string text)
        {
            Console.SetCursorPosition(leftSpacing + board.GetLength(0) * 4 + 8 + x, topSpacing + y);
            Console.Write(text);
        }
    }
}