package it.polimi.ingsw.gc14.Model;

import it.polimi.ingsw.gc14.Model.Cards.Building.EffectType;
import it.polimi.ingsw.gc14.Model.Cards.BuildingCard;
import it.polimi.ingsw.gc14.Model.Cards.TribeCard;
import it.polimi.ingsw.gc14.Model.Cards.TribeCards.Character;
import it.polimi.ingsw.gc14.Model.Cards.TribeCards.EventCard;
import it.polimi.ingsw.gc14.Model.Cards.TribeCards.EventType;
import it.polimi.ingsw.gc14.Model.GamePackage.Board;
import it.polimi.ingsw.gc14.Model.GamePackage.CurrentState;
import it.polimi.ingsw.gc14.Model.Orders.Order2;
import it.polimi.ingsw.gc14.Model.Orders.Order3;
import it.polimi.ingsw.gc14.Model.Orders.Order4;
import it.polimi.ingsw.gc14.Model.Orders.Order5;
import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;

import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import it.polimi.ingsw.gc14.Network.Observer;
/**
 * Represents the main game model.
 * A Game object stores the players, the current state of the match,
 * the slot assignments, the board, and the logic required to manage the game flow.
 */
public class Game implements Serializable {

    private transient List<Observer> observers = new ArrayList<>(); // transient! non serializzare

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    private void notifyObservers() {
        for (Observer o : observers) {
            o.update(this);
        }
    }
    /**
     * The current number of players.
     */
    public int getCurrentPlayerNumber() {
        return playersList.size();
    }

    /**
     * The list of players participating in the game.
     */
    private ArrayList<Player> playersList;

    /**
     * The current state of the game.
     */
    private CurrentState currentState;

    /**
     * The mapping between slots and the players assigned to them.
     */
    private HashMap<Slot,Player> slotMap;

    /**
     * The configured number of players for this game.
     */
    private int nPlayers;

    /**
     * The queue of players involved in optional card resolution.
     */
    private Queue<Player> OptionalCardQueue;

    /**
     * The order logic card associated with this game.
     */
    private OrderLogicCard orderLogicCard;

    /**
     * The board associated with this game.
     */
    private Board board;

    /**
     * Returns clones of the upper tribe cards currently available on the board.
     *
     * @return a list containing clones of the upper tribe cards currently available on the board.
     */
    public List<TribeCard>getUpperListTribeCards() {
        List<TribeCard> cards = new ArrayList<>();
        board.upperListTribe.forEach(x->cards.add(x.clone()));
        return cards;
    }

    /**
     * Returns clones of the lower tribe cards currently available on the board.
     *
     * @return a list containing clones of the lower tribe cards currently available on the board.
     */
    public List<TribeCard>getLowerListTribeCards() {
        List<TribeCard> cards = new ArrayList<>();
        board.lowerListTribe.forEach(x->cards.add(x.clone()));
        return cards;
    }

    /**
     * Returns clones of the upper building cards currently available on the board.
     *
     * @return a list containing clones of the upper building cards currently available on the board.
     */
    public List<BuildingCard>getUpperListBuilding() {
        List<BuildingCard> cards = new ArrayList<>();
        board.upperListBuilding.forEach(x->cards.add(x.clone()));
        return cards;
    }

    /**
     * Returns clones of the lower building cards currently available on the board.
     *
     * @return a list containing clones of the lower building cards currently available on the board.
     */
    public List<BuildingCard>getLowerListBuilding() {
        List<BuildingCard> cards = new ArrayList<>();
        board.lowerListBuilding.forEach(x->cards.add(x.clone()));
        return cards;
    }

    /**
     * Returns the current state of the game.
     *
     * @return the current state of the game.
     */
    public CurrentState getCurrentState() {
        return currentState;
    };

    /**
     * Returns the player with the specified username, if present.
     *
     * @param Username the username of the player to search for.
     * @return the player with the specified username, or {@code null} if no such player exists.
     */
    public Player getPlayerByUsername(String Username) throws IndexOutOfBoundsException {
        return playersList.stream().filter(x->x.getUserName().equals(Username)).findFirst().orElse(null);
    }

    /**
     * Returns the configured number of players for this game.
     *
     * @return the configured number of players for this game.
     */
    public int getNPlayers() {
        return nPlayers;
    }

    /**
     * Creates a game with the specified number of players.
     *
     * @param nPlayers the configured number of players for the game.
     * @throws IllegalArgumentException if {@code nPlayers < 0} or {@code nPlayers > 5}.
     */
    public Game(int nPlayers) throws IllegalArgumentException{
        if(nPlayers < 0||nPlayers > 5)
            throw new IllegalArgumentException();
        this.nPlayers = nPlayers;
        board=new Board(nPlayers);
        slotMap = new LinkedHashMap<>();
        for(Slot s :board.getSlotList())
        {
            slotMap.put(s,null);
        }
        currentState= new CurrentState();
        playersList = new ArrayList<>();
        OptionalCardQueue = new LinkedList<>();
    }

    /**
     * Creates a game with 0 configured players.
     */
    public Game()
    {
        this(0);
    }

    /**
     * Attempts to add the specified player to the game.
     * The operation succeeds only if the configured number of players is not 0,
     * the current game stage is {@code WAITING}, and the player is not already present.
     * If the number of players reaches the configured maximum, the game is initialized.
     *
     * @param player the player to add to the game.
     * @return {@code true} if the player is successfully added, {@code false} otherwise.
     */
    public boolean addPlayer(Player player) {
        if(this.nPlayers==0)
        {
            return  false;
        }
        if(currentState.getGameStage()!= GameStages.WAITING)
            return false;
        if(getPlayerByUsername(player.getUserName())!=null)
            return false;
        playersList.add(player);
        if(playersList.size()>=nPlayers)
        {
            init();
        }
        return true;
    }

    /**
     * Initializes the game after all required players have been added.
     * The method creates the appropriate order logic card according to the number of players,
     * selects the first current player, and updates the game stage to {@code SLOT_CHOICE}.
     */
    public void init() {

        switch (nPlayers) {
            case 2:
                orderLogicCard=new Order2(playersList);
                break;
            case 3:
                orderLogicCard=new Order3(playersList);
                break;
            case 4:
                orderLogicCard=new Order4(playersList);
                break;
            case 5:
                orderLogicCard=new Order5(playersList);
                break;
        }
        currentState.PlayerUpdate(orderLogicCard.pull(),null);
        currentState.GameStageUpdate(GameStages.SLOT_CHOICE);
    }

    //region Controller Methods

    /**
     * Attempts to assign the slot at the specified index to the specified player.
     * The operation succeeds only if the index is valid, the current game stage is {@code SLOT_CHOICE},
     * the specified player is the current player, and the selected slot is not already assigned.
     * If the slot is successfully assigned, the next player setup is triggered.
     *
     * @param player the player performing the slot choice.
     * @param slotIndex the index of the selected slot.
     * @return {@code true} if the slot choice succeeds, {@code false} otherwise.
     */
    public boolean SlotChoiceByIndex(Player player, int slotIndex) {
        if(slotIndex<0 || slotIndex>=slotMap.size())
            return  false;
        if(currentState.getGameStage()!= GameStages.SLOT_CHOICE)
        {
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer()))
        {
            return false;
        }
        Map.Entry<Slot, Player> slotPlayerEntry = new ArrayList<>(slotMap.entrySet()).get(slotIndex);
        if(slotPlayerEntry.getValue()!=null)
        {
            return false;
        }
        slotMap.put(slotPlayerEntry.getKey(),player);
        nextPlayerSetup();
        return true;

    }

    //region Drawing Methods

    /**
     * Attempts to draw the upper tribe card at the specified index for the specified player.
     * The operation succeeds only if the index is valid, the game stage is {@code RESOLVING_ACTIONS},
     * the specified player is the current player, at least one upper card draw is still available,
     * and the selected tribe card is not an event card.
     * If successful, the card is inserted into the player's collection, removed from the board,
     * and the number of remaining upper draws is decremented.
     * If both upper and lower draws become zero, the next player setup is triggered.
     *
     * @param player the player performing the draw.
     * @param cardIndex the index of the upper tribe card to draw.
     * @return {@code true} if the draw succeeds, {@code false} otherwise.
     */
    public boolean DrawUpperTribeCardByIndex(Player player,int cardIndex) {
        if( cardIndex<0 || cardIndex >=board.upperListTribe.size())
            return false;
        if(currentState.getGameStage()!= GameStages.RESOLVING_ACTIONS)
        {
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer()))
        {
            return false;
        }
        if(currentState.getNUpper() <1)
            return false;
        TribeCard tribeCard = board.upperListTribe.get(cardIndex);
        if(tribeCard.IsEventCard())
            return false;

        Character tempCard = (Character) tribeCard;
        tempCard.insert(player);
        board.removeUpperTribeCard(tempCard);
        currentState.UpperDrawn();
        if(currentState.getNUpper() ==0 && currentState.getNLower() ==0)
            nextPlayerSetup();
        return true;

    }

    /**
     * Attempts to draw the lower tribe card at the specified index for the specified player.
     * The operation succeeds only if the index is valid, the game stage is {@code RESOLVING_ACTIONS},
     * the specified player is the current player, at least one lower card draw is still available,
     * and the selected tribe card is not an event card.
     * If successful, the card is inserted into the player's collection, removed from the board,
     * and the number of remaining lower draws is decremented.
     * If both lower and upper draws become zero, the next player setup is triggered.
     *
     * @param player the player performing the draw.
     * @param cardIndex the index of the lower tribe card to draw.
     * @return {@code true} if the draw succeeds, {@code false} otherwise.
     */
    public boolean DrawLowerTribeCardByIndex(Player player, int cardIndex) {
        if( cardIndex<0 || cardIndex >=board.lowerListTribe.size())
            return false;
        if(currentState.getGameStage()!= GameStages.RESOLVING_ACTIONS)
        {
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer()))
        {
            return false;
        }


        if(currentState.getNLower() <1)
            return false;
        TribeCard tribeCard = board.lowerListTribe.get(cardIndex);
        if(tribeCard.IsEventCard())
            return false;

        Character tempCard = (Character) tribeCard;
        tempCard.insert(player);
        board.removeLowerTribeCard(tempCard);
        currentState.LowerDrawn();
        if(currentState.getNLower() ==0 && currentState.getNUpper() ==0)
            nextPlayerSetup();
        return true;

    }

    /**
     * Attempts to draw the upper building card at the specified index for the specified player.
     * The operation succeeds only if the index is valid, the game stage is {@code RESOLVING_ACTIONS},
     * the specified player is the current player, at least one upper card draw is still available,
     * and the selected building card can be bought by the player.
     * If successful, the building card is removed from the board and the number of remaining upper draws is decremented.
     * If both upper and lower draws become zero, the next player setup is triggered.
     *
     * @param player the player performing the draw.
     * @param cardIndex the index of the upper building card to draw.
     * @return {@code true} if the draw succeeds, {@code false} otherwise.
     */
    public boolean DrawUpperBuildingCardByIndex(Player player,int cardIndex) {
        if( cardIndex<0 || cardIndex >=board.upperListBuilding.size())
            return false;
        if(currentState.getGameStage()!= GameStages.RESOLVING_ACTIONS)
        {
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer()))
        {
            return false;
        }
        BuildingCard buildingCard = board.upperListBuilding.get(cardIndex);
        if(currentState.getNUpper() <1)
            return false;
        if(buildingCard.buy(player))
        {
            currentState.UpperDrawn();
            board.removeUpperBuildingCard(buildingCard);
        }
        else
            return false;
        if(currentState.getNLower() ==0 && currentState.getNUpper() ==0)
            nextPlayerSetup();

        return true;
    }

    /**
     * Attempts to draw the lower building card at the specified index for the specified player.
     * The operation succeeds only if the index is valid, the game stage is {@code RESOLVING_ACTIONS},
     * the specified player is the current player, at least one lower card draw is still available,
     * and the selected building card can be bought by the player.
     * If successful, the building card is removed from the board and the number of remaining lower draws is decremented.
     * If both lower and upper draws become zero, the next player setup is triggered.
     *
     * @param player the player performing the draw.
     * @param cardIndex the index of the lower building card to draw.
     * @return {@code true} if the draw succeeds, {@code false} otherwise.
     */
    public boolean DrawLowerBuildingCardByIndex(Player player,int cardIndex) {
        if( cardIndex<0 || cardIndex >=board.lowerListBuilding.size())
            return false;
        if(currentState.getGameStage()!= GameStages.RESOLVING_ACTIONS)
        {
            return false;
        }
        
        if(!player.equals(currentState.getCurrentPlayer()))
        {
            return false;
        }
        BuildingCard buildingCard = board.lowerListBuilding.get(cardIndex);
        if(currentState.getNLower() <1)
            return false;
        if(buildingCard.buy(player))
        {
            currentState.LowerDrawn();
            board.removeLowerBuildingCard(buildingCard);
        }
        else
            return false;
        if(currentState.getNLower() ==0 && currentState.getNUpper() ==0)
            nextPlayerSetup();

        return true;

    }

    //endregion

    //region Optional Card Methods


    /**
     * Attempts to pick the upper optional tribe card at the specified index for the specified player.
     * The operation succeeds only if the current game stage is {@code OPTIONAL_CARD_EFFECT},
     * the specified player is the current player, the index is valid,
     * and the selected tribe card is not an event card.
     * If successful, the card is inserted into the player's collection,
     * removed from the board, the player is removed from the optional card queue,
     * and the next player setup is triggered.
     *
     * @param player the player performing the optional tribe card pick.
     * @param cardIndex the index of the upper optional tribe card to pick.
     * @return {@code true} if the operation succeeds, {@code false} otherwise.
     */
    public boolean PickOptionalTribeCardByIndex(Player player,int cardIndex) {
        if(currentState.getGameStage() != GameStages.OPTIONAL_CARD_EFFECT){
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer())){
            return false;
        }

        if( cardIndex<0 || cardIndex >=board.upperListTribe.size())
            return false;
        TribeCard tribeCard = board.upperListTribe.get(cardIndex);

        if(tribeCard.IsEventCard())
            return false;

        Character tempCard = (Character) tribeCard;
        tempCard.insert(player);
        board.removeUpperTribeCard(tempCard);
        OptionalCardQueue.removeIf(x->x.equals(player));
        nextPlayerSetup();
        return true;
    }

    /**
     * Attempts to pick the upper optional building card at the specified index for the specified player.
     * The operation succeeds only if the current game stage is {@code OPTIONAL_CARD_EFFECT},
     * the specified player is the current player, the index is valid,
     * and the selected building card can be bought by the player.
     * If successful, the card is removed from the board, the next player setup is triggered,
     * and the player is removed from the optional card queue.
     *
     * @param player the player performing the optional building card pick.
     * @param cardIndex the index of the upper optional building card to pick.
     * @return {@code true} if the operation succeeds, {@code false} otherwise.
     */
    public boolean PickOptionalBuildingCard(Player player, int cardIndex) {
        if(currentState.getGameStage() != GameStages.OPTIONAL_CARD_EFFECT){
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer())){
            return false;
        }
        if( cardIndex<0 || cardIndex >=board.upperListBuilding.size())
            return false;
        BuildingCard buildingCard = board.upperListBuilding.get(cardIndex);
        if(buildingCard.buy(player)) {
            board.removeUpperBuildingCard(buildingCard);
        }
        else {
            return false;
        }
        nextPlayerSetup();
        OptionalCardQueue.removeIf(x->x.equals(player));
        return true;
    }


    /**
     * Skips the optional card choice for the specified player.
     * The operation succeeds only if the current game stage is {@code OPTIONAL_CARD_EFFECT}
     * and the specified player is the current player.
     * If successful, the player is removed from the optional card queue
     * and the next player setup is triggered.
     *
     * @param player the player skipping the optional card choice.
     * @return {@code true} if the operation succeeds, {@code false} otherwise.
     */
    public boolean NoOptionalCard(Player player) {
        if(currentState.getGameStage() != GameStages.OPTIONAL_CARD_EFFECT){
            return false;
        }
        if(!player.equals(currentState.getCurrentPlayer())){
            return false;
        }
        OptionalCardQueue.removeIf(x->x.equals(player));
        nextPlayerSetup();
        return true;
    }
    //endregion

    //endregion

    /**
     * Prepares the next player and updates the game state according to the current game stage.
     * If the current stage is {@code SLOT_CHOICE}, the next player is taken from the order logic card.
     * If no player is available, the game stage is updated to {@code RESOLVING_ACTIONS}
     * and the first assigned slot is selected.
     * If the current stage is {@code RESOLVING_ACTIONS}, the current player is pushed back
     * into the order logic card, the current slot is freed, and the next assigned slot is selected.
     * If no assigned slots remain, the game stage is updated to {@code OPTIONAL_CARD_EFFECT},
     * the optional card queue is built from players owning building cards with effect id equal to 12,
     * and the first player in that queue is selected.
     * If no player is available for optional card resolution, the game stage is updated to
     * {@code RESOLVING_EVENT}; then, if the round number is less than 10, the next round is prepared,
     * otherwise event resolution is performed, the game stage is updated to {@code ENDING},
     * and the game is ended.
     * If the current stage is {@code OPTIONAL_CARD_EFFECT}, the next player is taken from the optional card queue.
     * If no player is available, the game stage is updated to {@code RESOLVING_EVENT}.
     */
    private void nextPlayerSetup() {
        if(GameStages.SLOT_CHOICE==currentState.getGameStage()) {
            Player tempPlayer = orderLogicCard.pull();

            if(tempPlayer!=null) {
                currentState.PlayerUpdate(tempPlayer, null);
                return;
            }
            currentState.GameStageUpdate(GameStages.RESOLVING_ACTIONS);
            for (Map.Entry<Slot,Player> s : slotMap.entrySet()) {
                if (s.getValue()!=null) {
                    currentState.PlayerUpdate(s.getValue(), s.getKey());
                    return;
                }
            }
            return;

        }
        if(GameStages.RESOLVING_ACTIONS==currentState.getGameStage()) {
            orderLogicCard.push(currentState.getCurrentPlayer());
            slotMap.put(currentState.getSlot(), null);
            for (Slot s : slotMap.keySet()) {
                if (slotMap.get(s) != null) {
                    currentState.PlayerUpdate(slotMap.get(s), s);
                    break;
                }
            }
            if(slotMap.values().stream().allMatch(v -> v == null))
            {
                currentState.GameStageUpdate(GameStages.OPTIONAL_CARD_EFFECT);
                HashMap<Player,Integer> optional=new LinkedHashMap<>();
                for (Player p : playersList) {
                    int tempCount=(int)p.buildingCards.stream().filter(x->x.getEffectId()==12).count();
                    if(tempCount>0)
                    {
                        optional.put(p,tempCount);
                    }
                }
                OptionalCardQueue=new LinkedList<>();
                for(Map.Entry<Player,Integer> e : optional.entrySet())
                {
                    OptionalCardQueue.add(e.getKey());
                }
                currentState.PlayerUpdate(OptionalCardQueue.remove(), null);
                if(currentState.getCurrentPlayer()==null)
                {
                    currentState.GameStageUpdate(GameStages.RESOLVING_EVENT);
                    if(currentState.getRound()<10)
                    {
                        nextRound();
                    }
                    else
                    {
                        EventResolution();
                        currentState.GameStageUpdate(GameStages.ENDING);
                        endGame();
                    }

                    return;
                }
            }
        }
        if(GameStages.OPTIONAL_CARD_EFFECT==currentState.getGameStage()) {
            currentState.PlayerUpdate(OptionalCardQueue.remove(), null);
            if(currentState.getCurrentPlayer()==null)
            {
                currentState.GameStageUpdate(GameStages.RESOLVING_EVENT);
            }
        }
    }

    /**
     * Resolves all pending event cards if the current game stage is {@code RESOLVING_EVENT}.
     * All pending events are activated on the player list.
     * Event cards of type {@code SUSTENANCE} are resolved after all other pending events.
     */
    private void EventResolution() {

        if(currentState.getGameStage()!= GameStages.RESOLVING_EVENT)
        {
            return;
        }
        Queue<EventCard> events;
        events=board.getPendingEvents();
        ArrayList<EventCard>sustenance=events.stream().filter(x->x.getType().equals(EventType.SUSTENANCE)).collect(Collectors.toCollection(ArrayList::new));
        events.removeAll(sustenance);
        for(EventCard event:events)
        {
            event.activateEvent(playersList);
        }

        for(EventCard e : sustenance)
        {
            e.activateEvent(playersList);
        }

    }


    /**
     * Advances the game to the next round.
     * The method first resolves pending events.
     * If the current round is 10, the game stage is updated to {@code ENDING} and the game is ended.
     * Otherwise, the era is updated if the board changes era, and the round number is incremented.
     */
    private void nextRound() {

        EventResolution();
        if(currentState.getRound()==10)
        {
            currentState.GameStageUpdate(GameStages.ENDING);
            endGame();
        }
        if(currentState.getEra()!= board.nextRound())
            currentState.EraUpdate();
        currentState.RoundUpdate();

    }

    /**
     * Ends the game by applying all final building effects owned by each player
     * and updating the game stage to {@code ENDED}.
     */
    private void endGame() {
        playersList.forEach(
            p -> p.buildingCards.stream().filter(x -> x.getEffectType() == EffectType.FINAL).
            forEach(x -> x.applyEffect(p))
        );
        currentState.GameStageUpdate(GameStages.ENDED);
    }

    /**
     * Sets the configured number of players for this game.
     * The operation succeeds only if the current configured number of players is 0.
     *
     * @param nPlayers the new configured number of players.
     * @return {@code true} if the number of players is updated, {@code false} otherwise.
     */
    public boolean setNPlayer(int nPlayers)
    {
        if(this.nPlayers!=0)
            return false;
        this.nPlayers=nPlayers;
        return true;
    }

}
