using LittleTown.Core.Actions;
using LittleTown.Core.Exceptions;
using LittleTown.Core.Ports;

namespace LittleTown.Core;

/// <summary>
/// Represente un match de LittleTown
/// </summary>
public class Match
{
    /// <summary> LE numero du tour en cours (Partant de 1) </summary>
    public int CurrentTurn { get; private set; } = 1;

    /// <summary> l'id du joueur a qui c'est le tour de jouer</summary>
    public string CurrentPlayer { get => _players[_playerTurnsOrder[_currentPlayerIndex]].PlayerName; }

    /// <summary> Indique si le match est terminé </summary>
    public bool IsDone { get; private set; }

    /// <summary>la liste indiquant l'ordre des joueurs, _playerTurnsOrder[0] donne l'index du 1er joueur, _playerTurnsOrder[1] du second..... </summary>
    private List<int> _playerTurnsOrder = new List<int>();

    private const int _minPlayerCount = 2;
    private const int _maxPlayerCount = 4;

    private int _maxWorkerPerPlayer;
    private int _maxBuidingPerPlayer;

    private readonly Board _board;
    private ICollection<Building> _buildings;
    private ICollection<Objective> _objectives;

    private Random _random = new Random();

    private List<PlayerZone> _players = new();
    private int _currentPlayerIndex;

    /// <summary>
    /// Constructeur d'une nouvelle partie avec un nombre de joueurs données en parametres
    /// </summary>
    /// <param name="staticData">un objet permettant de récupérer les données statiques du jeu</param>
    public Match(IStaticDataGetter staticData)
    {
        ArgumentNullException.ThrowIfNull(staticData);

        _board = staticData.GetBoardAsync(1).GetAwaiter().GetResult();
        _buildings = staticData.GetBuildings();
        _objectives = staticData.GetObjectives();
    }

    /// <summary> Ajouter un nouveau joueur a la partie </summary>
    /// <param name="playerName">le nom du joueur</param>
    public void AddPlayer(string playerName)
    {
        if (_players.Count < _maxPlayerCount)
        {
            if (_players.Any(p => p.PlayerName == playerName))
            {
                throw new MatchConfigException("Un joueur existe déjà avec ce nom");
            }
            _players.Add(new PlayerZone()
            {
                PlayerName = playerName
            });
        }
        else
        {
            throw new MatchConfigException("Impossible d'ajouter de nouveau joueur, la partie est pleine");
        }
    }


    /// <summary>
    /// Demander au match d'executer une action si elle est autorisée
    /// </summary>
    /// <param name="action">l'action a réaliser</param>
    public void ExecuteAction(IAction action)
    {
        ArgumentNullException.ThrowIfNull(action);

        //quelques vérification génériques pour savoir si l'action peut être jouée
        if (IsDone)
        {
            throw new MatchFinishedException("Impossible d'effectuer une action sur un match terminé");
        }

        //autoriser l'action a s'executer en lui donner les getters dont elle a besoin
        action.Execute(this);
    }

    /// <summary> Initialiser la partie, il faut avoir ajouté les joueurs au préalable </summary>
    /// <exception cref="MatchConfigException"></exception>
    public void Init()
    {
        int nbPlayer = _players.Count;

        ArgumentOutOfRangeException.ThrowIfLessThan(nbPlayer, _minPlayerCount);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(nbPlayer, _maxPlayerCount);

        List<int> freeObjectiveIndexs = Enumerable.Range(0, _objectives.Count).ToList();
        foreach (PlayerZone zone in _players)
        {
            zone.AddObjectives(GetRandomObjectives(nbPlayer switch
            {
                2 => 4,
                3 => 3,
                4 => 2,
                _ => throw new MatchConfigException("Mauvais nombre de joueurs lors Workers")
            }, freeObjectiveIndexs));
            zone.AddRessources(Enums.ResourceType.Piece, 3);
        }

        _maxWorkerPerPlayer = nbPlayer switch
        {
            2 => 5,
            3 => 4,
            4 => 3,
            _ => throw new MatchConfigException("Mauvais nombre de joueurs lors Workers")
        };

        _maxBuidingPerPlayer = nbPlayer switch
        {
            2 => 7,
            3 => 6,
            4 => 6,
            _ => throw new MatchConfigException("Mauvais nombre de joueurs lors building")
        };

        // preparer l'ordre des joueurs
        int index = _random.Next(nbPlayer);
        for (int i = 0; i < nbPlayer; ++i)
        {
            _playerTurnsOrder.Add(index++);
            if (index >= nbPlayer)
                index = 0;
        }

        _currentPlayerIndex = 0;

    }

    /// <summary> Permet de récuperer une player zone(une copie) </summary>
    /// <param name="playerName">le nom ou ID du joueur</param>
    /// <returns></returns>
    public PlayerZone GetPlayerZone(string playerName)
    {
        var value = _players.Where(p => p.PlayerName == playerName).FirstOrDefault();


        if (null == value)
            throw new ArgumentException("playerID is out of bound");

        return value.Clone() as PlayerZone ?? throw new ArgumentException("Cast exception in GetPlayerZone"); ;
    }

    private List<Objective> GetRandomObjectives(int number, List<int> freeIndex)
    {
        List<Objective> result = new List<Objective>();
        for (int i = 0; i < number; i++)
        {
            int randomIndex = _random.Next(freeIndex.Count);
            int cardIndex = freeIndex[randomIndex];
            freeIndex.RemoveAt(randomIndex);
            result.Add(_objectives.ElementAt(randomIndex));
        }
        return result;
    }
    /// <summary> Changer le joueur en cours pour passer au suivant </summary>
    public void NextPlayer()
    {
        _currentPlayerIndex++;
        if (_currentPlayerIndex >= _players.Count)
        {
            _currentPlayerIndex = 0;
            CurrentTurn++;
        }
        if (CurrentTurn >= 4)
        {
            IsDone = true;
        }
    }
}