package ch.zhaw.catan;

import ch.zhaw.hexboard.HexBoard;
import ch.zhaw.hexboard.Label;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Subclass of HexBoard
 * Saves the fields which are set and handles Methods with specific Dice Numbers.
 */
public class SiedlerBoard extends HexBoard<Config.Land, Settlement, Road, String> {

    /**
     * HashMap to save all Fields which are set yet.
     * Key: Point with coordinates of the field
     * Value: Field Object
     */
    private final HashMap<Point, Field> fields = new HashMap<>();
    private Config.Faction longestRoadFaction = null;
    private int longestRoadLength = 0;

    /**
     * Method to create the predefined game field from Config.
     */
    public void createFixGameField() {
        Map<Point, Config.Land> resourcePlacement = Config.getStandardLandPlacement();
        Map<Point, Integer> dicePlacement = Config.getStandardDiceNumberPlacement();
        for (Map.Entry<Point, Config.Land> resourceField : resourcePlacement.entrySet()) {
            addField(resourceField.getKey(), resourceField.getValue());
            if (dicePlacement.get(resourceField.getKey()) != null) {
                String numberAsString = dicePlacement.get(resourceField.getKey()).toString();
                char[] numbersInChar = numberAsString.toCharArray();
                if (numberAsString.length() < 2) {
                    fields.put(resourceField.getKey(), new Field(resourceField.getValue(), new Label('0', numbersInChar[0])));
                } else {
                    fields.put(resourceField.getKey(), new Field(resourceField.getValue(), new Label(numbersInChar[0], numbersInChar[1])));
                }
            }
        }
    }

    /**
     * Get Paramter longestRoad
     *
     * @return longestRoadLength
     */
    public int getLongestRoadLength() {
        return longestRoadLength;
    }

    /**
     * Method to get the DiceNumber of a specific field.
     *
     * @param field Point with coordinates of the specific field
     * @return the DiceNumber of the field.
     */
    private int getDiceNumber(Point field) {
        Label label = fields.get(field).getLabel();
        return Integer.parseInt(label.toString());
    }

    /**
     * Method to create a SiedlerBoardTextView Object set the LowerFieldLabels with theirs dice number.
     * It is used to print the actual board in TextIO.
     *
     * @return String of actual board.
     */
    public String toString() {
        SiedlerBoardTextView view = new SiedlerBoardTextView(this);
        for (Map.Entry<Point, Field> field : fields.entrySet()) {
            view.setLowerFieldLabel(field.getKey(), field.getValue().getLabel());
        }
        return view.toString();
    }

    /**
     * Returns the fields associated with the specified dice value.
     *
     * @param dice the dice value
     * @return the fields associated with the dice value
     */
    public List<Point> getFieldsForDiceValue(int dice) {
        ArrayList<Point> fields = new ArrayList<>();
        for (Point field : this.fields.keySet()) {
            if (getDiceNumber(field) == dice) {
                fields.add(field);
            }
        }
        return fields;
    }

    /**
     * Method to get the Resources which are paid to a specific faction for a specific field.
     *
     * @param point   The Point with the Coordinates of the field, which should pay resources.
     * @param faction The faction, which should get paid.
     * @return a ArrayList with all resources, which will be paid to the specified faction.
     */
    public ArrayList<Config.Resource> getResourcesForFaction(Point point, Config.Faction faction) {
        List<Settlement> possibleSettlementField = super.getCornersOfField(point);
        ArrayList<Config.Resource> resourcesToPlayer = new ArrayList<>();
        for (Settlement settlement : possibleSettlementField) {
            if (settlement.getFaction() == faction) {
                resourcesToPlayer.add(fields.get(point).getResource());
                if (settlement instanceof City) {
                    resourcesToPlayer.add(fields.get(point).getResource());
                }
            }
        }
        return resourcesToPlayer;
    }

    /**
     * Returns the {@link Config.Land}s adjacent to the specified corner.
     *
     * @param corner the corner
     * @return the list with the adjacent {@link Config.Land}s
     */
    public List<Config.Land> getLandsForCorner(Point corner) {
        Point above = new Point(corner.x, corner.y + 2);
        Point below = new Point(corner.x, corner.y - 2);
        Config.Land[] lands = new Config.Land[3];
        if (hasField(above)) {
            lands[0] = getField(above);
            lands[1] = getField(new Point(corner.x + 1, corner.y - 1));
            lands[2] = getField(new Point(corner.x - 1, corner.y - 1));
        } else if (hasField(below)) {
            lands[0] = getField(below);
            lands[1] = getField(new Point(corner.x + 1, corner.y + 1));
            lands[2] = getField(new Point(corner.x - 1, corner.y + 1));
        } else {
            return Collections.emptyList();
        }
        return List.of(lands);
    }

    /**
     * This method checks for the player with the longest road according to the Siedler game rules.
     *
     * @param factionList a List with all factions which can place structures on the board
     * @return the faction who owns the longest road with minimum length of 5, null there is no road longer then 4
     */
    public Config.Faction getLongestRoadFaction(List<Config.Faction> factionList) {
        List<Settlement> corners = getCorners();
        HashMap<Config.Faction, Integer> players = new HashMap<>();

        for (Config.Faction faction : factionList) {
            int count = 0;
            players.put(faction, count);
            for (Settlement settlement : corners) {
                if (settlement.getFaction() == faction) {
                    HashSet<Road> roads = new HashSet<>();
                    roads = countRoad(faction, settlement.getPosition(), roads, true);
                    count = roads.size();
                    int currentCount = players.get(faction);
                    if (count > currentCount) {
                        players.put(faction, count);
                    }
                }
            }
        }

        for (Config.Faction factionA : players.keySet()) {
            if (players.get(factionA) > longestRoadLength && players.get(factionA) > 4) {
                longestRoadFaction = factionA;
                longestRoadLength = players.get(factionA);
            }
        }
        return longestRoadFaction;
    }

    /**
     * This method is recursive and adds all roads which belongs to a specific players and stringing together to a HashSet.
     * The length of the HashSet represents the length of the longest Road the player has.
     *
     * @param faction  the faction of the player to check on
     * @param position there has to be a starting point to start counting. In this case it's a corner where a settlement belonging to the player's faction is build on.
     * @param roads    is the hashset with all roads belong to the player which are stringing together
     * @param add      if true branches needs to be count together. (for example if it is the starting point(first time of counting)) otherwise the longest branch is being added to roads.
     * @return HashSet with all roads from a specific player which are string together.
     */
    private HashSet<Road> countRoad(Config.Faction faction, Point position, HashSet<Road> roads, boolean add) {
        List<Road> roadsList = getAdjacentEdges(position);
        //Checks if roads is interrupted by other players settlement
        if (getCorner(position) != null && getCorner(position).getFaction() != faction) {
            return roads;
        }
        //removes roads from roadsList which doesn't belong to the same player or which are allready added to roads.
        for (Road roadsRoad : roads) {
            Iterator<Road> it = roadsList.iterator();
            while (it.hasNext()) {
                Road roadsListRoad = it.next();
                if (roadsListRoad == roadsRoad || roadsListRoad.getFaction() != faction) {
                    it.remove();
                }
            }
        }
        if (roadsList.size() == 1) {
            roads.add(roadsList.get(0));
            position = getNextPoint(roadsList.get(0), position);
            roads = countRoad(faction, position, roads, false);
        } else if (roadsList.size() == 2) {
            ArrayList<HashSet<Road>> possibleRoads = checkRoadsSwitch(faction,2,roadsList,position);
            if (add) {
                possibleRoads.get(1).addAll(possibleRoads.get(0));
                roads.addAll(possibleRoads.get(1));
            } else {
                HashSet<Road> tallest;
                if (possibleRoads.get(0).size() >= possibleRoads.get(1).size()) {
                    tallest = possibleRoads.get(0);
                } else {
                    tallest = possibleRoads.get(1);
                }
                roads.addAll(tallest);
            }

        } else if (roadsList.size() == 3) {
            ArrayList<HashSet<Road>> possibleRoads = checkRoadsSwitch(faction,3,roadsList,position);
            HashSet<Road> tallest;
            HashSet<Road> secondTallest;
            if (possibleRoads.get(0).size() >= possibleRoads.get(1).size()) {
                tallest = possibleRoads.get(0);
                secondTallest = possibleRoads.get(1);
            } else {
                tallest = possibleRoads.get(1);
                secondTallest = possibleRoads.get(0);
            }
            if (possibleRoads.get(2).size() >= secondTallest.size()) {
                secondTallest = possibleRoads.get(2);
            }
            tallest.addAll(secondTallest);
            roads.addAll(tallest);
        }
        return roads;
    }

    /**
     * This method is being used to evaluate the next starting position to get the adjacent Roads from it.
     *
     * @param road     the next road to check on
     * @param position the current starting point
     * @return return the opposite point of the current point.
     */
    private Point getNextPoint(Road road, Point position) {
        Point start = road.getStart();
        Point end = road.getEnd();
        if (position.equals(start)) {
            position = end;
        } else {
            position = start;
        }
        return position;
    }

    /**
     * This method checks at a roads switch, which road starting at the switch will be the longest possibility.
     * @param faction the faction of the player to check on
     * @param numberOfSwitches how many adjacent roads there are at the corner
     * @param roadsList the list of adjacent roads
     * @param position the current corner from where to count further.
     * @return ArrayList of HashSet with all possible forks.
     */
    private ArrayList<HashSet<Road>> checkRoadsSwitch(Config.Faction faction,int numberOfSwitches,List<Road> roadsList,Point position) {
        ArrayList<HashSet<Road>> possibleRoads = new ArrayList<>();
        for (int i = 0;i<numberOfSwitches;i++) {
            HashSet<Road> list = new HashSet<>();
            list.add(roadsList.get(i));
            Point nextPosition = getNextPoint(roadsList.get(i),position);
            list = countRoad(faction, nextPosition, list, false);
            possibleRoads.add(list);
        }
        return possibleRoads;
    }
}
