using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Models.Events;
using Models.Exceptions;
using Models.Interfaces;
using Models.Rules;

namespace Models.Game
{
    /// <summary>
    /// The Game class represents a game session in the application.
    /// It contains all the necessary properties and methods to manage a game, including the game loop, dice rolling, and use of the game rules.
    /// </summary>
    public class Game
    {
        /* Persistence */
        public IPersistence PersistenceManager { get; set; }
        
        /* List for the game and persistence */
        public List<Player> Players { get; set; }
        public List<Game> Games { get; set; }
        public List<Map> Maps { get; set; }
        public List<BestScore> BestScores { get; set; }
        
        private bool _isRunning;
        public bool IsRunning
        {
            get => _isRunning;
            private set => _isRunning = value;
        }

        public Player CurrentPlayer { get; private set; }

        public Map UsedMap { get; private set; }

        public Dice Dice1 { get; private set; }
        public Dice Dice2 { get; private set; }

        public int Turn { get; private set; }

        public Rules.Rules GameRules { get; }

        
        // == Events ==
        public event EventHandler<GameStartedEventArgs> GameStarted;
        public event EventHandler<GameEndedEventArgs> GameEnded;
        public event EventHandler<BoardsUpdateEventArgs> BoardUpdated;
        public event EventHandler<DiceRolledEventArgs> DiceRolled;
        public event EventHandler<OperationChosenEventArgs> OperationChosen;
        public event EventHandler<CellChosenEventArgs> CellChosen;
        
        public void AddPlayer(Player player)
        {
            Players.Add(player);
        }
        
        public void AddGame(Game game)
        {
            Games.Add(game);
        }
        
        public void AddMap(Map map)
        {
            Maps.Add(map);
        }
        
        public void AddBestScore(BestScore bestScore)
        {
            BestScores.Add(bestScore);
        }

        public void LoadData()
        {
            var data = PersistenceManager.LoadData();
            foreach (var player in data.Item1)
            {
                Players.Add(player);
            }
            foreach (var game in data.Item2)
            {
                Games.Add(game);
            }
            foreach (var map in data.Item3)
            {
                Maps.Add(map);
            }
            foreach (var bestScore in data.Item4)
            {
                BestScores.Add(bestScore);
            }
        }
        
        public void SaveData() => PersistenceManager.SaveData(Players, Games, Maps, BestScores);
        
        public Game(IPersistence persistenceManager)
        {
            PersistenceManager = persistenceManager;
            
            Players = new List<Player>();
            Games = new List<Game>();
            Maps = new List<Map>();
            BestScores = new List<BestScore>();

            GameRules = new Rules.Rules();
            
            IsRunning = false;
        }

        public Game()
        {
            UsedMap = new Map("Map1");
            BestScores = new List<BestScore>();

            GameRules = new Rules.Rules();

            IsRunning = false;
        }

        /// <summary>
        /// Rolls all the dice.
        /// </summary>
        private void RollAllDice()
        {
            Dice1.Roll();
            Dice2.Roll();
            DiceRolled?.Invoke(this, new DiceRolledEventArgs(Dice1.Value, Dice2.Value));
        }
        
        /// <summary>
        /// Marks an operation as checked in the operation grid of the game.
        /// </summary>
        /// <param name="operation"></param>
        private void MarkOperationAsChecked(Operation operation)
        {
            int operationIndex = (int)operation;
            int operationsPerType = 4;  // Chaque type d'opération peut être fait 4 fois

            for (int i = operationIndex * operationsPerType; i < (operationIndex + 1) * operationsPerType; i++)
            {
                if (!UsedMap.OperationGrid[i].IsChecked)
                {
                    UsedMap.OperationGrid[i].Check();
                    break;
                }
            }
        }

        /// <summary>
        /// Performs an operation on the values of two dice based on the provided operation.
        /// </summary>
        /// <param name="o">The operation to perform. This can be LOWER, HIGHER, SUBTRACTION, ADDITION, or MULTIPLICATION.</param>
        /// <returns>
        /// The result of the operation. If the operation is LOWER or HIGHER, it returns the lower or higher value of the two dice respectively.
        /// If the operation is SUBTRACTION, it returns the difference between the higher and lower value of the two dice.
        /// If the operation is ADDITION, it returns the sum of the values of the two dice.
        /// If the operation is MULTIPLICATION, it returns the product of the values of the two dice.
        /// If the operation is not one of the operations, it throws an ArgumentOutOfRangeException.
        /// </returns>
        public int ResultOperation(Operation o)
        {
            int result = o switch
            {
                Operation.LOWER => Dice1.IsLower(Dice2) ? Dice1.Value : Dice2.Value,
                Operation.HIGHER => Dice1.IsLower(Dice2) ? Dice2.Value : Dice1.Value,
                Operation.SUBTRACTION => Dice1.IsLower(Dice2) ? Dice2.Value - Dice1.Value : Dice1.Value - Dice2.Value,
                Operation.ADDITION => Dice2.Value + Dice1.Value,
                Operation.MULTIPLICATION => Dice2.Value * Dice1.Value,
                _ => throw new ArgumentOutOfRangeException()
            };

            MarkOperationAsChecked(o);
            return result;
        }


        /// <summary>
        /// Places the result of a dice operation into a chosen cell on the game board.
        /// The result can be placed in the chosen cell if it's the first turn or if the chosen cell is valid according to the game rules.
        /// </summary>
        /// <param name="playerChoice">The cell chosen by the player to place the result.</param>
        /// <param name="result">The result of the dice operation to be placed in the cell.</param>
        private bool PlaceResult(Cell playerChoice, int result)
        {
            if (Turn == 1 || GameRules.NearCellIsValid(playerChoice, UsedMap.Boards.ToList()))
            {
                for (int i = 0; i < UsedMap.Boards.Count; i++)
                {
                    if (UsedMap.Boards[i].X == playerChoice.X && UsedMap.Boards[i].Y == playerChoice.Y)
                    {
                        if (UsedMap.Boards[i].Value != null)
                            return false;
                        UsedMap.Boards[i].Value = result;
                        BoardUpdated?.Invoke(this, new BoardsUpdateEventArgs(UsedMap.Boards.ToList()));
                        return true;
                    }
                }
                //playerChoice.Value = result;
            }
            return false;
            
        }

        /// <summary>
        /// Add the choosen cell to a rope path if it's possible.
        /// </summary>
        /// <param name="playerChoice"></param>
        /// <param name="adjacentes"></param>
        private void AddToRopePath(Cell playerChoice,List<Cell> adjacentes)
        {
            int index =0;

            foreach (var cells in adjacentes.Where(cells => cells.Value - playerChoice.Value == 1 || cells.Value - playerChoice.Value == -1))
            {
                // Le cas si il n'existe aucun chemin de corde
                if (UsedMap.RopePaths.Count == 0)
                {
                    // Creer un nouveau chemin de corde avec la cellule choisi par le joueur et celle adjacente
                    UsedMap.RopePaths.Add(new List<Cell> {playerChoice, cells});
                }


                // A modifier dans le cas ou il est possible de fusionner deux chemins de corde
                if (GameRules.IsInRopePaths(playerChoice, UsedMap.RopePaths, index)) break;

                // Le cas si il existe des chemins de corde

                // Est-ce que la cellule adjacentes fait parti d'un chemin de corde
                if (!GameRules.IsInRopePaths(cells,UsedMap.RopePaths,index))
                {
                    UsedMap.RopePaths.Add(new List<Cell> { playerChoice, cells });
                    continue;
                }

                if (!GameRules.AsValue(playerChoice,UsedMap.RopePaths,index))
                {
                    UsedMap.RopePaths[index].Add(playerChoice);
                }
            }
        }

        /// <summary>
        /// Initializes the game.
        /// </summary>
        public void InitializeGame(Map map, Player player, bool startImmediately = true)
        {
            UsedMap = map;
            CurrentPlayer = player;
            Turn = 1;
            Dice1 = new Dice();
            Dice2 = new Dice(1);

            if (startImmediately)
            {
                StartGame();
            }
        }

        /// <summary>
        /// Starts the game.
        /// </summary>
        private void StartGame()
        {
            IsRunning = true;
            GameStarted?.Invoke(this, new GameStartedEventArgs(CurrentPlayer));
            GameLoop();
        }

        /// <summary>
        /// Ends the game.
        /// </summary>
        private void EndGame(int? pts)
        {
            IsRunning = false;
            GameEnded?.Invoke(this, new GameEndedEventArgs(CurrentPlayer, pts));
        }
        
        /// <summary>
        /// The main game loop that runs while the game is active.
        /// </summary>
        private void GameLoop()
        {
            while (IsRunning)
            {
                if (Turn == 20)
                {
                    foreach(var cells in UsedMap.Boards)
                    {
                        GameRules.IsZoneValidAndAddToZones(cells, UsedMap);
                        AddToRopePath(cells, GameRules.EveryAdjacentCells(cells, UsedMap.Boards.ToList()));
                    }
                    int? points = GameRules.FinalCalculusOfZones(UsedMap.Zones);
                    for (int i = 0; i < UsedMap.RopePaths.Count; i++)
                    {
                        points += GameRules.ScoreRopePaths(UsedMap.RopePaths[i]);
                    }
                    EndGame(points);
                    break;
                }
                
                RollAllDice();

                Turn++;
            }
        }
        
        /// <summary>
        /// Handles the player's choice of an operation based on the dice values.s
        /// </summary>
        /// <param name="operation">The operation chosen by the player.</param>
        public void HandlePlayerOperation(Operation operation)
        {
            int result = ResultOperation(operation);
            OperationChosen?.Invoke(this, new OperationChosenEventArgs(operation, result));
        }

        /// <summary>
        /// Handles the player's choice of a cell on the game board.
        /// </summary>
        /// <param name="cell"></param>
        /// <param name="result"></param>
        /// <exception cref="InvalidCellCoordinatesException"></exception>
        /// <exception cref="InvalidCellException"></exception>
        public bool HandlePlayerChoice(Cell cell, int result)
        {
            if (cell.X < 0 || cell.X >= UsedMap.Boards.Count / 6 || cell.Y < 0 || cell.Y >= 6)
            {
                return false;
                //throw new InvalidCellCoordinatesException("Invalid cell coordinates. Please choose again.");
            }

            if (!GameRules.IsCellValid(cell, UsedMap.Boards.ToList()))
            {
                return false;
                //throw new InvalidCellException("Cell is not valid. Please choose again.");
            }

            bool res = PlaceResult(cell, result);
            if (!res)
            {
                return false;
                //throw new InvalidPlaceResultException("Cell is not valid for place result. Please choose again.");
            }
            GameRules.IsZoneValidAndAddToZones(cell, UsedMap);
            AddToRopePath(cell, GameRules.EveryAdjacentCells(cell, UsedMap.Boards.ToList()));
            CellChosen?.Invoke(this, new CellChosenEventArgs(cell, result));
            return true;
            //BoardUpdated?.Invoke(this, EventArgs.Empty);
        }
    }
}
