package ch.zhaw.catan;

import java.awt.Point;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;


/**
 * This class performs all actions related to modifying the game state.
 *
 * @author Andrin Fassbind, Leonardo Brandenberger, Roman Schenk, Stefan Amador
 */
public class SiedlerGame {
    static final int FOUR_TO_ONE_TRADE_OFFER = 4;
    static final int FOUR_TO_ONE_TRADE_WANT = 1;

    private final SiedlerBoard board;
    private final ArrayList<Player> allPlayers;
    private final int winPointsForWin;
    private final Bank bank;
    private int activePlayer;

    /**
     * Constructs a SiedlerGame game state object.
     *
     * @param winPoints       the number of points required to win the game
     * @param numberOfPlayers the number of players
     * @throws IllegalArgumentException if winPoints is lower than 3
     *                                  or players is not between two and four
     */
    public SiedlerGame(int winPoints, int numberOfPlayers) {
        bank = new Bank();
        board = new SiedlerBoard();
        board.createFixGameField();
        allPlayers = new ArrayList<>();
        createPlayer(numberOfPlayers);
        activePlayer = 0;
        this.winPointsForWin = winPoints;
    }

    private void createPlayer(int numberOfPlayers) {
        for (int i = 0; i < numberOfPlayers; i++) {
            allPlayers.add(new Player(Config.Faction.values()[i]));
        }
    }

    /**
     * Switches to the next player in the defined sequence of players.
     */
    public void switchToNextPlayer() {
        if (activePlayer < allPlayers.size() - 1) {
            activePlayer++;
        } else if (activePlayer == allPlayers.size() - 1) {
            activePlayer = 0;
        }
    }

    /**
     * Switches to the previous player in the defined sequence of players.
     */
    public void switchToPreviousPlayer() {
        if (activePlayer > 0) {
            activePlayer--;
        } else if (activePlayer == 0) {
            activePlayer = allPlayers.size() - 1;
        }
    }

    /**
     * This methode is used to add resources to the player.
     *
     * @param player      the active Player
     * @param resource    the resource to add
     * @param numberToAdd the quantity of resources to add
     * @return true if resource has been added else false
     */
    private boolean addResourcesToPlayer(Player player, Config.Resource resource, int numberToAdd) {
        if (bank.getResourceFromBank(resource, numberToAdd)) {
            player.addResource(resource, numberToAdd);
            return true;
        }
        return false;
    }

    /**
     * This methode is used to subtract resources from Player
     *
     * @param player           the active player
     * @param resource         the resource to subtract
     * @param numberToSubtract the quantity of resource to subtract
     * @return true if resource has been subtracted
     */
    private boolean subtractResourceFromPlayer(Player player, Config.Resource resource, int numberToSubtract) {
        if (player.subtractResource(resource, numberToSubtract)) {
            bank.storeResourceToBank(resource, numberToSubtract);
            return true;
        }
        return false;
    }

    /**
     * Returns the {@link Config.Faction}s of the active players.
     *
     * <p>The order of the player's factions in the list must
     * correspond to the oder in which they play.
     * Hence, the player that sets the first settlement must be
     * at position 0 in the list etc.
     *
     * <strong>Important note:</strong> The list must contain the
     * factions of active players only.</p>
     *
     * @return the list with player's factions
     */
    public List<Config.Faction> getPlayerFactions() {
        List<Config.Faction> factions = new ArrayList<>();
        for (Player player : allPlayers) {
            factions.add(player.getFaction());
        }
        return factions;
    }

    /**
     * Returns the game board. Used for test
     *
     * @return the game board
     */
    public SiedlerBoard getBoard() {
        return board;
    }

    /**
     * Returns the String used to show the Board
     *
     * @return String of the Board.
     */
    public String getBoardTextView(){return board.toString();}

    /**
     * Returns the {@link Config.Faction} of the current player.
     *
     * @return the faction of the current player
     */
    public Config.Faction getCurrentPlayerFaction() {
        return allPlayers.get(activePlayer).getFaction();
    }

    /**
     * Returns how many resource cards of the specified type
     * the current player owns.
     *
     * @param resource the resource type
     * @return the number of resource cards of this type
     */
    public int getCurrentPlayerResourceStock(Config.Resource resource) {
        return allPlayers.get(activePlayer).getSpecificResource(resource);
    }

    /**
     * Returns the resources of the current player.
     *
     * @return a hashmap with all resources the player has. Key: Resource name Value: number of resources
     */
    public HashMap<Config.Resource, Integer> getCurrentPlayerResource() {
        return allPlayers.get(activePlayer).getResources();
    }

    /**
     * Places a settlement in the founder's phase (phase II) of the game.
     *
     * <p>The placement does not cost any resource cards. If payout is
     * set to true, for each adjacent resource-producing field, a resource card of the
     * type of the resource produced by the field is taken from the bank (if available) and added to
     * the players' stock of resource cards.</p>
     *
     * @param position the position of the settlement
     * @param payout   if true, the player gets one resource card per adjacent resource-producing field
     * @return true, if the placement was successful
     */
    public boolean placeInitialSettlement(Point position, boolean payout) {
        if (!validPositionForSettlement(position)) {
            return false;
        }
        board.setCorner(position, new Settlement(allPlayers.get(activePlayer).getFaction(), position));
        if (payout) {
            List<Config.Land> lands = board.getLandsForCorner(position);
            for (Config.Land land : lands) {
                if (land.getResource() != null) {
                    addResourcesToPlayer(allPlayers.get(activePlayer), land.getResource(), 1);
                }
            }
        }
        return true;
    }

    /**
     * Places a road in the founder's phase (phase II) of the game.
     * The placement does not cost any resource cards.
     *
     * @param roadStart position of the start of the road
     * @param roadEnd   position of the end of the road
     * @return true, if the placement was successful
     */
    public boolean placeInitialRoad(Point roadStart, Point roadEnd) {
        if (!validPositionForRoad(roadStart, roadEnd)) {
            return false;
        }
        board.setEdge(roadStart, roadEnd, new Road(allPlayers.get(activePlayer).getFaction(), roadStart, roadEnd));
        return true;
    }

    /**
     * This method takes care of actions depending on the dice throw result.
     * <p>
     * A key action is the payout of the resource cards to the players
     * according to the payout rules of the game. This includes the
     * "negative payout" in case a 7 is thrown and a player has more than
     * {@link Config#MAX_CARDS_IN_HAND_NO_DROP} resource cards.
     * <p>
     * If a player does not get resource cards, the list for this players'
     * {@link Config.Faction} is <b>an empty list (not null)</b>!.
     *
     * <p>
     * The payout rules of the game take into account factors such as, the number
     * of resource cards currently available in the bank, settlement types
     * (settlement or city), and the number of players that should get resource
     * cards of a certain type (relevant if there are not enough left in the bank).
     * </p>
     *
     * @param diceThrow the resource cards that have been distributed to the players
     * @return the resource cards added to the stock of the different players
     */
    public Map<Config.Faction, List<Config.Resource>> throwDice(int diceThrow) {
        if (diceThrow == 7) {
            for (Player player : allPlayers) {
                handleDiceThrow7(player);
            }
        } else {
            Map<Config.Faction, List<Config.Resource>> returnMap = new HashMap<>();
            List<Point> diceValueFields = board.getFieldsForDiceValue(diceThrow);
            for (Player player : allPlayers) {
                returnMap.put(player.getFaction(), new ArrayList<>());
                for (Point field : diceValueFields) {
                    List<Config.Resource> resources = board.getResourcesForFaction(field, player.getFaction());
                    for (Config.Resource resource : resources) {
                        returnMap.get(player.getFaction()).add(resource);
                        addResourcesToPlayer(player, resource, 1);
                    }
                }
            }
            return returnMap;
        }
        return null;
    }

    /**
     * This method handles the case if a 7 has been diced.
     *
     * @param player the active player who rolls the dice.
     */
    public void handleDiceThrow7(Player player) {
        ArrayList<Config.Resource> resourceArrayList = new ArrayList<>();
        HashMap<Config.Resource, Integer> resources = player.getResources();
        for (Config.Resource resource : resources.keySet()) {
            for (int i = 0; i < resources.get(resource); i++) {
                resourceArrayList.add(resource);
            }
        }
        if (resourceArrayList.size() > Config.MAX_CARDS_IN_HAND_NO_DROP) {
            int resourcesToRemove = resourceArrayList.size() - (resourceArrayList.size() / 2);
            Random random = new Random();
            for (int i = 0; i < resourcesToRemove; i++) {
                subtractResourceFromPlayer(player, resourceArrayList.remove(random.nextInt(resourceArrayList.size())), 1);
            }
        }
    }


    /**
     * Builds a settlement at the specified position on the board.
     *
     * <p>The settlement can be built if:
     * <ul>
     * <li> the player possesses the required resource cards</li>
     * <li> a settlement to place on the board</li>
     * <li> the specified position meets the build rules for settlements</li>
     * </ul>
     *
     * @param position the position of the settlement
     * @return true, if the placement was successful
     */
    public boolean buildSettlement(Point position) {
        //1. Check if position is corner && is empty && neighbour Corners are empty
        if (!validPositionForSettlement(position)) {
            return false;
        }
        //2. Check if neighbourEdge are Roads belong to active Player
        if (!checkAdjacentEdgesList(position)) {
            return false;
        }
        //3. Can Player build Settlement
        if (!allPlayers.get(activePlayer).build(Config.Structure.SETTLEMENT)) {
            return false;
        }

        List<Config.Resource> costs = Config.Structure.SETTLEMENT.getCosts();
        for (Config.Resource resource : costs) {
            subtractResourceFromPlayer(allPlayers.get(activePlayer), resource, 1);
        }

        //4. Insert Settlement to map
        board.setCorner(position, new Settlement(allPlayers.get(activePlayer).getFaction(), position));
        return true;
    }

    /**
     * Builds a city at the specified position on the board.
     *
     * <p>The city can be built if:
     * <ul>
     * <li> the player possesses the required resource cards</li>
     * <li> a city to place on the board</li>
     * <li> the specified position meets the build rules for cities</li>
     * </ul>
     *
     * @param position the position of the city
     * @return true, if the placement was successful
     */
    public boolean buildCity(Point position) {
        //1. Check if Corner.
        if (!board.hasCorner(position)) {
            return false;
        }
        //2. Check if Settlement has already been built
        Settlement atCurrentPosition = board.getCorner(position);
        if (atCurrentPosition == null || atCurrentPosition instanceof City || atCurrentPosition.getFaction() != allPlayers.get(activePlayer).getFaction()) {
            return false;
        }
        //3. Can player build a City.
        if (!allPlayers.get(activePlayer).build(Config.Structure.CITY)) {
            return false;
        }

        List<Config.Resource> costs = Config.Structure.CITY.getCosts();
        for (Config.Resource resource : costs) {
            subtractResourceFromPlayer(allPlayers.get(activePlayer), resource, 1);
        }

        //4.Insert City into the map.
        board.setCorner(position, new City(allPlayers.get(activePlayer).getFaction(), position));
        return true;

    }

    /**
     * Builds a road at the specified position on the board.
     *
     * <p>The road can be built if:
     * <ul>
     * <li> the player possesses the required resource cards</li>
     * <li> a road to place on the board</li>
     * <li> the specified position meets the build rules for roads</li>
     * </ul>
     *
     * @param roadStart the position of the start of the road
     * @param roadEnd   the position of the end of the road
     * @return true, if the placement was successful
     */
    public boolean buildRoad(Point roadStart, Point roadEnd) {
        //1. Check if is edge && is empty && if neighbour Edge or Corners belong to Settlement of active Player
        if (!validPositionForRoad(roadStart, roadEnd)) {
            return false;
        }
        //2. Can Player build road
        if (!allPlayers.get(activePlayer).build(Config.Structure.ROAD)) {
            return false;
        }

        List<Config.Resource> costs = Config.Structure.ROAD.getCosts();
        for (Config.Resource resource : costs) {
            subtractResourceFromPlayer(allPlayers.get(activePlayer), resource, 1);
        }

        //3. Insert Road to map
        board.setEdge(roadStart, roadEnd, new Road(allPlayers.get(activePlayer).getFaction(), roadStart, roadEnd));
        return true;
    }


    /**
     * This Method is used to check if the chosen position for a road is valid or not.
     *
     * @param roadStart the coordinates where the road begins.
     * @param roadEnd   the coordinates where the road ends.
     * @return true if road position is valid otherwise false
     */
    private boolean validPositionForRoad(Point roadStart, Point roadEnd) {
        //1. Check if it is an edge
        if (!board.hasEdge(roadStart, roadEnd)) {
            return false;
        }
        //2. Check if edge is empty
        if (board.getEdge(roadStart, roadEnd) != null) {
            return false;
        }
        //3. Check if neighbouring edges are roads
        boolean hasNeighbourRoad = (checkAdjacentEdgesList(roadStart) || checkAdjacentEdgesList(roadEnd));
        if (hasNeighbourRoad) {
            return true;
        }
        //4.Check if roadStart or roadEnd is Settlement of current player
        return (board.getCorner(roadStart) != null && board.getCorner(roadStart).getFaction() == allPlayers.get(activePlayer).getFaction())
                || (board.getCorner(roadEnd) != null && board.getCorner(roadEnd).getFaction() == allPlayers.get(activePlayer).getFaction());
    }

    /**
     * Can be used for both initial Settlement and normal Phase.
     *
     * @param position the position on the board to check for valid settlement position
     * @return true if valid position for settlement
     */
    private boolean validPositionForSettlement(Point position) {
        //1. Check if corner
        if (!board.hasCorner(position)) {
            return false;
        }
        //2. Check if water
        if (checkIfWater(position)) {
            return false;
        }
        //3. Check if corner is empty
        if (board.getCorner(position) != null) {
            return false;
        }
        //3. Check if neighbouring corners are empty
        return checkAdjacentCornerList(position);
    }

    private boolean checkIfWater(Point point) {
        List<Config.Land> fields = board.getFields(point);
        for (Config.Land land : fields) {
            if (!land.equals(Config.Land.WATER)) {
                return false;
            }
        }
        return true;
    }

    /**
     * This method checks if there are Roads build by active Player on adjacent edges
     *
     * @param point point to check on
     * @return true if there is a road build next to the point.
     */
    private boolean checkAdjacentEdgesList(Point point) {
        List<Road> results = board.getAdjacentEdges(point);
        for (Road result : results) {
            if (result.getFaction() == allPlayers.get(activePlayer).getFaction()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if Adjacent Corners are empty
     *
     * @param point Corner to check
     * @return true if all Neighbour Corners are empty
     */
    private boolean checkAdjacentCornerList(Point point) {
        List<Settlement> results = board.getNeighboursOfCorner(point);
        return results.size() <= 0;
    }

    /**
     * Trades in {@link #FOUR_TO_ONE_TRADE_OFFER} resource cards of the
     * offered type for {@link #FOUR_TO_ONE_TRADE_WANT} resource cards of the wanted type.
     * <p>
     * The trade only works when bank and player possess the resource cards
     * for the trade before the trade is executed.
     *
     * @param offer offered type
     * @param want  wanted type
     * @return true, if the trade was successful
     */
    public boolean tradeWithBankFourToOne(Config.Resource offer, Config.Resource want) {
        Player player = allPlayers.get(activePlayer);
        if (player.getSpecificResource(offer) >= FOUR_TO_ONE_TRADE_OFFER && addResourcesToPlayer(player, want, FOUR_TO_ONE_TRADE_WANT)) {
            subtractResourceFromPlayer(player, offer, FOUR_TO_ONE_TRADE_OFFER);
            return true;
        }
        return false;
    }

    /**
     * Returns the winner of the game, if any.
     *
     * @return the winner of the game or null, if there is no winner (yet)
     */
    public Config.Faction getWinner() {
        if (getCurrentPlayerWinPoints() >= winPointsForWin) {
            return getCurrentPlayerFaction();
        }
        return null;
    }

    /**
     * This methode counts the winpoints of the current player.
     *
     * @return the winpoints as an integer
     */
    public int getCurrentPlayerWinPoints() {
        int winPoints = 0;
        List<Settlement> settlements = board.getCorners();
        for (Structure structure : settlements) {

            int newWinPoints = 0;
            if (structure instanceof City) {
                newWinPoints = 2;
            } else if (structure instanceof Settlement) {
                newWinPoints = 1;
            }
            if (structure.getFaction() == getCurrentPlayerFaction()) {
                winPoints += newWinPoints;
            }
        }
        if (getCurrentPlayerFaction() == board.getLongestRoadFaction(getPlayerFactions())) {
            winPoints += 2;
        }
        return winPoints;
    }

    /**
     * Places the thief on the specified field and steals a random resource card (if
     * the player has such cards) from a random player with a settlement at that
     * field (if there is a settlement) and adds it to the resource cards of the
     * current player.
     *
     * @param field the field on which to place the thief
     * @return false, if the specified field is not a field or the thief cannot be
     * placed there (e.g., on water)
     */
    public boolean placeThiefAndStealCard(Point field) {
        // Implemented longest road.
        return false;
    }

}
