@SuppressWarnings("unchecked")
@Log4j
//This class is a @Singleton
public final class CivSingleton {

    private static CivSingleton civSingleton = new CivSingleton();
    //Key is playerId and value is username
    private LoadingCache<String, String> usernameCache;

    private Map<GameType, ItemReader> itemsCache = new ConcurrentHashMap<>();

    private Cache<String, String> chatCache;

    private CivSingleton() {
        this.chatCache = CacheBuilder.<String, String>newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build();
    }

    public static CivSingleton instance() {
        return civSingleton;
    }

    public void setPlayerCache(LoadingCache<String, String> usernameCache) {
        this.usernameCache = usernameCache;
    }

    /**
     * Key is playerId and value is username
     */
    public LoadingCache<String, String> playerCache() {
        return usernameCache;
    }

    /**
     * Key is a combination of GameType and SheetName
     *
     * @return
     */
    public Map<GameType, ItemReader> itemsCache() {
        return itemsCache;
    }

    public Cache<String, String> getChatCache() {
        return chatCache;
    }

} @NoArgsConstructor
@JsonRootName("chat")
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class ChatDTO {
    private String id;
    private String pbfId;
    private String username;
    private String message;
    private String color;
    private long created;

    public ChatDTO(String id, String pbfId, String username, String message, String color, long createdInMillis) {
        this.id = id;
        this.pbfId = pbfId;
        this.username = username;
        this.message = message;
        this.created = createdInMillis;
        this.color = color;
    }

    public ChatDTO(String username, String message, long createdInMillis) {
        this.username = username;
        this.message = message;
        this.created = createdInMillis;
    }

    public ChatDTO(Chat chat) {
        this.created = chat.getCreatedInMillis();
        this.id = chat.getId();
        this.message = chat.getMessage();
        this.username = chat.getUsername();
        this.pbfId = chat.getPbfId();
    }
} @JsonIgnoreProperties(ignoreUnknown = true)
@Data
@NoArgsConstructor
public class CivHighscoreDTO {
    private List<WinnerDTO> winners = new ArrayList<>();
    private List<WinnerDTO> fiveWinners = new ArrayList<>();
    private List<WinnerDTO> fourWinners = new ArrayList<>();
    private List<WinnerDTO> threeWinners = new ArrayList<>();
    private List<WinnerDTO> twoWinners = new ArrayList<>();
    private int totalNumberOfPlayers;
    private int totalNumberOfGames;
    private long fivePlayerGamesTotal;
    private long fourPlayerGamesTotal;
    private long threePlayerGamesTotal;
    private long twoPlayerGamesTotal;
} @Data
@JsonRootName(value = "createNewGame")
public class CreateNewGameDTO {
    @NotEmpty
    public String name;

    @NotNull
    private GameType type;

    @NotNull
    @Min(2)
    @Max(5)
    private Integer numOfPlayers;

    @NotNull
    private String color;
} @NoArgsConstructor
@JsonRootName(value = "draw")
@Data
public class DrawDTO {

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime created;
    /**
     * The user that made the draw. Its always a player that initiates a draw, so this cannot be blank.
     */
    @NotBlank
    private String playerId;
    /**
     * A draw must always belong to a game. The pbf game id
     */
    @NotBlank
    private String pbfId;
    /**
     * If null, then no undo has been performed
     */
    private Undo undo = null;
    private boolean hidden;

    public DrawDTO(Draw draw) {
        if (draw != null) {
            this.playerId = draw.getPlayerId();
            this.pbfId = draw.getPbfId();
            this.undo = draw.getUndo();
            this.created = draw.getCreated();
            this.hidden = draw.getItem() == null || draw.getItem().isHidden();
        }
    }

    /**
     * Returns true if undo has been requested
     */
    @JsonIgnore
    private boolean isUndoInitiated() {
        return undo != null;
    }

} @Data
@JsonRootName("gameDTO")
@JsonIgnoreProperties(ignoreUnknown = true)
public class GameDTO {

    private String id;
    private GameType type;
    private String name;
    private long created;
    private String whosTurnIsIt; //username of the players turn
    private boolean active;
    private String mapLink;
    private String assetLink;

    private List<GameLogDTO> publicLogs;
    private List<GameLogDTO> privateLogs;
    private Playerhand player;
    private List<Item> revealedItems;

} @JsonRootName("gameLogDTO")
@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class GameLogDTO {
    private String id;
    private String log;
    private long created;
    private DrawDTO draw;

    public GameLogDTO(String id, String log, long created, DrawDTO draw) {
        this.id = id;
        this.log = log;
        this.created = created;
        this.draw = draw;
    }
} public class MessageDTO {

    private final String message;

    @JsonCreator
    public MessageDTO(@JsonProperty("msg") String message) {
        this.message = message;
    }

    @JsonProperty("msg")
    public String getMessage() {
        return message;
    }

} @Data
@JsonRootName("pbfDTO")
@JsonIgnoreProperties(ignoreUnknown = true)
public class PbfDTO {
    @NotEmpty
    private String id;

    @NotBlank
    private String name;
    private GameType type;

    private long created = System.currentTimeMillis();

    private int numOfPlayers;
    private boolean active = true;

    private List<PlayerDTO> players = new ArrayList<>();
    private String nameOfUsersTurn;

} @JsonRootName("playerDTO")
@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class PlayerDTO {
    private String playerId;

    @NotNull
    private String username;

    @Email
    @NotNull
    private String email;

    @NotBlank
    @NotNull
    private String password;

    private String pbfId;

} @JsonIgnoreProperties(ignoreUnknown = true)
@Data
@NoArgsConstructor
public class PlayerHighscoreDTO {
    private List<WinnerDTO> winners = new ArrayList<>();
    private List<WinnerDTO> fiveWinners = new ArrayList<>();
    private List<WinnerDTO> fourWinners = new ArrayList<>();
    private List<WinnerDTO> threeWinners = new ArrayList<>();
    private List<WinnerDTO> twoWinners = new ArrayList<>();
    private int totalNumberOfPlayers;
    private int totalNumberOfGames;
    private long fivePlayerGamesTotal;
    private long fourPlayerGamesTotal;
    private long threePlayerGamesTotal;
    private long twoPlayerGamesTotal;
} @JsonIgnoreProperties(ignoreUnknown = true)
@Data
@NoArgsConstructor
public class WinnerDTO implements Comparable<WinnerDTO> {
    private String username;
    private int totalWins;
    private long attempts;
    private String percentWin;

    public WinnerDTO(String username, int totalWins, long attempts) {
        this.username = username;
        this.totalWins = totalWins;
        this.attempts = attempts;
        this.percentWin = percentWin();
    }

    @Override
    public int compareTo(WinnerDTO o) {
        int i = Integer.compare(this.getTotalWins(), o.getTotalWins());
        if (i != 0) {
            return i;
        }

        return username.compareTo(o.getUsername());
    }

    @JsonIgnore
    private String percentWin() {
        if (totalWins == 0 || attempts == 0)
            return "0 %";
        double percent = ((double) totalWins / (double) attempts) * 100;
        double twoDecimals = (double) Math.round(percent * 100) / 100;
        return twoDecimals + " %";
    }
} @Log4j
public class SendEmail {
    public static final String SENDGRID_USERNAME = "SENDGRID_USERNAME";
    public static final String SENDGRID_PASSWORD = "SENDGRID_PASSWORD";
    public static final String NOREPLY_PLAYCIV_COM = "noreply@playciv.com";
    public static final String URL = "http://playciv.com/";
    public static final String REST_URL = "https://civilization-boardgame.herokuapp.com/";
    public static final String CASH_EMAIL = "cash@playciv.com";
    private static final SendGrid sendgrid = new SendGrid(System.getenv(SENDGRID_USERNAME), System.getenv(SENDGRID_PASSWORD));

    public static String gamelink(String pbfId) {
        return URL + "#/game/" + pbfId;
    }

    public static boolean sendYourTurn(String gamename, String emailToo, String pbfId) {
        if (System.getenv(SENDGRID_USERNAME) == null || System.getenv(SENDGRID_PASSWORD) == null) {
            log.error("Missing environment variable for SENDGRID_USERNAME or SENDGRID_PASSWORD");
            return false;
        }
        SendGrid.Email email = new SendGrid.Email();
        email.addTo(emailToo);
        email.setFrom(NOREPLY_PLAYCIV_COM);
        email.setSubject("It is your turn");
        email.setText("It's your turn to play in " + gamename + "!\n\n" +
                "Go to " + gamelink(pbfId) + " to start your turn");

        try {
            SendGrid.Response response = sendgrid.send(email);
            return response.getStatus();
        } catch (SendGridException e) {
            log.error("Error sending email: " + e.getMessage(), e);
        }
        return false;
    }

    public static boolean sendMessage(String email, String subject, String message, String playerId) {
        if (System.getenv(SENDGRID_USERNAME) == null || System.getenv(SENDGRID_PASSWORD) == null) {
            log.error("Missing environment variable for SENDGRID_USERNAME or SENDGRID_PASSWORD");
            return false;
        }
        SendGrid.Email sendGridEmail = new SendGrid.Email();
        sendGridEmail.addTo(email);
        sendGridEmail.setFrom(NOREPLY_PLAYCIV_COM);
        sendGridEmail.setSubject(subject);
        sendGridEmail.setText(message + UNSUBSCRIBE(playerId));

        try {
            SendGrid.Response response = sendgrid.send(sendGridEmail);
            return response.getStatus();
        } catch (SendGridException e) {
            log.error("Error sending sendGridEmail: " + e.getMessage(), e);
        }
        return false;
    }

    public static boolean someoneJoinedTournament(Player player) {
        if (System.getenv(SENDGRID_USERNAME) == null || System.getenv(SENDGRID_PASSWORD) == null) {
            log.error("Missing environment variable for SENDGRID_USERNAME or SENDGRID_PASSWORD");
            return false;
        }

        SendGrid.Email sendGridEmail = new SendGrid.Email();
        sendGridEmail.addTo(CASH_EMAIL);
        sendGridEmail.setFrom(NOREPLY_PLAYCIV_COM);
        sendGridEmail.setSubject(player.getUsername() + " joined tournament");
        sendGridEmail.setText(player.getUsername() + " with email " + player.getEmail() + " joined the tournament");

        try {
            SendGrid.Response response = sendgrid.send(sendGridEmail);
            sendConfirmationToPlayer(player);
            return response.getStatus();
        } catch (SendGridException e) {
            log.error("Error sending sendGridEmail: " + e.getMessage(), e);
        }

        return false;
    }

    private static void sendConfirmationToPlayer(Player player) {
        SendGrid.Email sendGridEmail = new SendGrid.Email();
        sendGridEmail.addTo(player.getEmail());
        sendGridEmail.setFrom(CASH_EMAIL);
        sendGridEmail.setSubject("You joined the tournament");
        sendGridEmail.setText("You have just entered the 1vs1 tournament. " +
                "To confirm your participation please donate the appropriate amount. You can find the donate link on playciv.com website. " +
                "Afterwards please reply to this email and let me know that you have donated. Good luck and have fun!");

        try {
            sendgrid.send(sendGridEmail);
        } catch (SendGridException e) {
            log.error("Error sending sendGridEmail: " + e.getMessage(), e);
        }
    }

    private static String UNSUBSCRIBE(String playerId) {
        if (Strings.isNullOrEmpty(playerId)) {
            return "";
        }
        return "\n\nIf you wish to unsubscribe from ALL emails, then push this link: " + REST_URL + "api/admin/email/notification/" + playerId + "/stop";
    }

} @Log4j
public class ItemReader {
    private static final Predicate<Cell> notEmptyPredicate = cell -> !cell.toString().isEmpty();
    private static final Predicate<Cell> notRandomPredicate = cell -> !cell.toString().equals("RAND()");
    private static final Predicate<Cell> rowNotZeroPredicate = cell -> cell.getRow().getRowNum() != 0;
    private static final Predicate<Cell> columnIndexZeroPredicate = cell -> cell.getColumnIndex() == 0;
    public static AtomicInteger itemCounter = new AtomicInteger(RandomUtils.nextInt(1, 20));
    public LinkedList<Civ> shuffledCivs;
    public LinkedList<CultureI> shuffledCultureI;
    public LinkedList<CultureII> shuffledCultureII;
    public LinkedList<CultureIII> shuffledCultureIII;
    public LinkedList<GreatPerson> shuffledGPs;
    public LinkedList<Hut> shuffledHuts;
    public LinkedList<Village> shuffledVillages;
    public LinkedList<Wonder> modernWonders;
    public LinkedList<Wonder> medievalWonders;
    public LinkedList<Wonder> ancientWonders;
    public LinkedList<Tile> shuffledTiles;
    public LinkedList<Citystate> shuffledCityStates;
    public List<Tech> allTechs; //Not linked list because all players can choose the same tech
    public LinkedList<Aircraft> aircraftList;
    public LinkedList<Artillery> artilleryList;
    public LinkedList<Mounted> mountedList;
    public LinkedList<Infantry> infantryList;
    public List<SocialPolicy> socialPolicies;
    public ImmutableList<Item> redrawableItems;

    private static Infantry createInfantry(String string) {
        Iterable<String> split = split(string);

        int attack = Integer.parseInt(Iterables.get(split, 0));
        int health = Integer.parseInt(Iterables.get(split, 1));

        return new Infantry(attack, health);
    }

    private static Artillery createArtillery(String string) {
        Iterable<String> split = split(string);
        int attack = Integer.parseInt(Iterables.get(split, 0));
        int health = Integer.parseInt(Iterables.get(split, 1));

        return new Artillery(attack, health);
    }

    private static Mounted createMounted(String string) {
        Iterable<String> split = split(string);
        int attack = Integer.parseInt(Iterables.get(split, 0));
        int health = Integer.parseInt(Iterables.get(split, 1));

        return new Mounted(attack, health);
    }

    private static Aircraft createAircraft(String string) {
        Iterable<String> split = split(string);
        int attack = Integer.parseInt(Iterables.get(split, 0));
        int health = Integer.parseInt(Iterables.get(split, 1));

        return new Aircraft(attack, health);
    }

    private static Iterable<String> split(String string) {
        return Splitter.onPattern(",|\\.").omitEmptyStrings().trimResults().split(string);
    }

    @SuppressWarnings("unchecked")
    public void readItemsFromExcel(GameType gameType) throws IOException {
        InputStream in;
        switch (gameType) {
            case WAW:
                in = getClass().getClassLoader().getResourceAsStream("assets/gamedata-faf-waw.xlsx");
                break;
            case FAF:
                throw new IOException("FAF not supported yet");
            case BASE:
                throw new IOException("Base is not supported yet");
            case DOC:
                throw new IOException("DoC is not supported yet");
            default:
                throw new IOException("For now we only support WAW");
        }

        try (Workbook wb = new XSSFWorkbook(in)) {
            shuffledCivs = (LinkedList<Civ>) getShuffledCivsFromExcel(wb);
            shuffledCultureI = (LinkedList<CultureI>) getShuffledCultureIFromExcel(wb);
            shuffledCultureII = (LinkedList<CultureII>) getShuffledCultureIIFromExcel(wb);
            shuffledCultureIII = (LinkedList<CultureIII>) getShuffledCultureIIIFromExcel(wb);
            shuffledGPs = (LinkedList<GreatPerson>) getShuffledGreatPersonFromExcel(wb);
            shuffledHuts = (LinkedList<Hut>) getShuffledHutsFromExcel(wb);
            shuffledVillages = (LinkedList<Village>) getShuffledVillages(wb);
            shuffledCityStates = (LinkedList<Citystate>) getShuffledCityStates(wb);
            shuffledTiles = (LinkedList<Tile>) getShuffledTilesFromExcel(wb);
            extractShuffledWondersFromExcel(wb);
            allTechs = getTechsFromExcel(wb);
            readInfantry(wb);
            readMounted(wb);
            readArtillery(wb);
            readAircraft(wb);
            socialPolicies = (LinkedList<SocialPolicy>) getSocialPolicies(wb);
            redrawableItems = ImmutableList.<Item>builder()
                    .addAll(shuffledCultureI)
                    .addAll(shuffledCultureII)
                    .addAll(shuffledCultureIII)
                    .addAll(infantryList)
                    .addAll(mountedList)
                    .addAll(artilleryList)
                    .addAll(aircraftList)
                    .addAll(shuffledGPs)
                    .build();
        }
    }

    private LinkedList<? extends Item> getShuffledCityStates(Workbook wb) {
        Sheet civSheet = wb.getSheet(SheetName.CITY_STATES.getName());

        List<Cell> unfilteredCSCells = new ArrayList<>();
        civSheet.forEach(row -> row.forEach(unfilteredCSCells::add));

        List<Citystate> cityStates = unfilteredCSCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(cs -> new Citystate(cs.toString()))
                .collect(Collectors.toList());

        List<String> description = unfilteredCSCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as city states
        for (int i = 0; i < cityStates.size(); i++) {
            Citystate item = cityStates.get(i);
            item.setDescription(description.get(i));
        }

        Collections.shuffle(cityStates);
        return new LinkedList<>(cityStates);
    }

    private LinkedList<? extends Item> getShuffledCivsFromExcel(Workbook wb) {
        Sheet civSheet = wb.getSheet(SheetName.CIV.toString());

        List<Cell> unfilteredCivCells = new ArrayList<>();
        civSheet.forEach(row -> row.forEach(unfilteredCivCells::add));

        List<Civ> civs = unfilteredCivCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(civname -> new Civ(civname.toString()))
                .collect(Collectors.toList());

        List<String> startingTech = unfilteredCivCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        List<String> descriptionCells = unfilteredCivCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 2)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as cultures
        for (int i = 0; i < civs.size(); i++) {
            Civ item = civs.get(i);
            item.setDescription(descriptionCells.get(i));
            item.setStartingTech(new Tech(startingTech.get(i), Tech.LEVEL_1, itemCounter.incrementAndGet()));
        }

        Collections.shuffle(civs);
        return new LinkedList<>(civs);
    }

    private LinkedList<? extends Item> getShuffledCultureIFromExcel(Workbook wb) {
        Sheet culture1Sheet = wb.getSheet(SheetName.CULTURE_1.getName());

        List<Cell> unfilteredCells = new ArrayList<>();
        culture1Sheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<CultureI> cultures = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(cell -> new CultureI(cell.toString()))
                .collect(Collectors.toList());


        List<String> description = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as cultures
        for (int i = 0; i < cultures.size(); i++) {
            CultureI item = cultures.get(i);
            item.setDescription(description.get(i));
        }

        Collections.shuffle(cultures);
        return new LinkedList<>(cultures);
    }

    private LinkedList<? extends Item> getShuffledCultureIIFromExcel(Workbook wb) {
        Sheet culture2Sheet = wb.getSheet(SheetName.CULTURE_2.getName());

        List<Cell> unfilteredCells = new ArrayList<>();
        culture2Sheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<CultureII> culture2s = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(cell -> new CultureII(cell.toString()))
                .collect(Collectors.toList());

        List<String> description = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as culture2s
        for (int i = 0; i < culture2s.size(); i++) {
            CultureII item = culture2s.get(i);
            item.setDescription(description.get(i));
        }

        Collections.shuffle(culture2s);
        return new LinkedList<>(culture2s);
    }

    private LinkedList<? extends Item> getShuffledCultureIIIFromExcel(Workbook wb) {
        Sheet culture3Sheet = wb.getSheet(SheetName.CULTURE_3.getName());

        List<Cell> unfilteredCells = new ArrayList<>();
        culture3Sheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<CultureIII> culture3s = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(cell -> new CultureIII(cell.toString()))
                .collect(Collectors.toList());

        List<String> description = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as culture3s
        for (int i = 0; i < culture3s.size(); i++) {
            CultureIII item = culture3s.get(i);
            item.setDescription(description.get(i));
        }

        Collections.shuffle(culture3s);
        return new LinkedList<>(culture3s);
    }

    private LinkedList<? extends Item> getShuffledGreatPersonFromExcel(Workbook wb) {
        Sheet gpSheet = wb.getSheet(SheetName.GREAT_PERSON.getName());

        List<Cell> unfilteredCells = new ArrayList<>();
        gpSheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<GreatPerson> gps = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(cell -> new GreatPerson(cell.toString()))
                .collect(Collectors.toList());

        List<String> tile = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        List<String> description = unfilteredCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 2)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as cultures
        for (int i = 0; i < gps.size(); i++) {
            GreatPerson item = gps.get(i);
            item.setDescription(description.get(i));
            item.setType(tile.get(i));
        }

        Collections.shuffle(gps);

        //Now we want to take every other one
        LinkedList<GreatPerson> gpLinkedList = new LinkedList<>(gps);
        return gpLinkedList;
    }

    private void extractShuffledWondersFromExcel(Workbook wb) {
        Sheet wonderSheet = wb.getSheet(SheetName.WONDERS.getName());

        List<Cell> unfilteredCells = new ArrayList<>();
        wonderSheet.forEach(row -> row.forEach(unfilteredCells::add));

        //Kategoriser wonderne

        List<String> wonderName = unfilteredCells.stream()
                .filter(p -> !p.toString().trim().isEmpty())
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(Object::toString)
                .collect(Collectors.toList());

        List<String> description = unfilteredCells.stream()
                .filter(p -> !p.toString().trim().isEmpty())
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        LinkedList<String> wondersName = new LinkedList<>(wonderName);
        LinkedList<String> descriptions = new LinkedList<>(description);

        //Kun ancient
        ancientWonders = new LinkedList<>();
        //There is no break in java 8 forEach, thus we use the old for
        for (int i = 0; i < wondersName.size(); i++) {
            String wonder = wondersName.poll();
            String desc = descriptions.poll();
            if (wonder.toLowerCase().contains(SheetName.WONDERS.getName().toLowerCase())) {
                break;
            }
            ancientWonders.add(new Wonder(wonder, desc, Wonder.ANCIENT, SheetName.ANCIENT_WONDERS));
        }
        Collections.shuffle(ancientWonders);

        //Kun ancient
        medievalWonders = new LinkedList<>();
        for (int i = 0; i < wondersName.size(); i++) {
            String wonder = wondersName.poll();
            String desc = descriptions.poll();
            if (wonder.toLowerCase().contains(SheetName.WONDERS.getName().toLowerCase())) {
                break;
            }
            medievalWonders.add(new Wonder(wonder, desc, Wonder.MEDIEVAL, SheetName.MEDIEVAL_WONDERS));
        }
        Collections.shuffle(medievalWonders);

        //Only modern left
        modernWonders = new LinkedList<>();

        int remainingSize = wondersName.size();

        for (int i = 0; i < remainingSize; i++) {
            String wonder = wondersName.poll();
            String desc = descriptions.poll();
            modernWonders.add(new Wonder(wonder, desc, Wonder.MODERN, SheetName.MODERN_WONDERS));
        }
        Collections.shuffle(modernWonders);
    }

    private LinkedList<? extends Item> getShuffledTilesFromExcel(Workbook wb) {
        Sheet tileSheet = wb.getSheet(SheetName.TILES.getName());

        List<Cell> unfilteredTileCells = new ArrayList<>();
        tileSheet.forEach(row -> row.forEach(unfilteredTileCells::add));

        List<Tile> tiles = unfilteredTileCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(tilename -> new Tile(String.format("%d", (int) Double.valueOf(tilename.toString()).doubleValue())))
                .collect(Collectors.toList());

        Collections.shuffle(tiles);
        return new LinkedList<>(tiles);
    }

    private LinkedList<? extends Item> getShuffledHutsFromExcel(Workbook wb) {
        Sheet hutSheet = wb.getSheet(SheetName.HUTS.getName());

        List<Cell> unfilteredCivCells = new ArrayList<>();
        hutSheet.forEach(row -> row.forEach(unfilteredCivCells::add));

        List<Hut> huts = unfilteredCivCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(hut -> new Hut(hut.toString()))
                .collect(Collectors.toList());

        Collections.shuffle(huts);
        return new LinkedList<>(huts);
    }

    private LinkedList<? extends Item> getShuffledVillages(Workbook wb) {
        Sheet sheet = wb.getSheet(SheetName.VILLAGES.getName());

        List<Cell> unfilteredCivCells = new ArrayList<>();
        sheet.forEach(row -> row.forEach(unfilteredCivCells::add));

        List<Village> villages = unfilteredCivCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(village -> new Village(village.toString()))
                .collect(Collectors.toList());

        Collections.shuffle(villages);
        return new LinkedList<>(villages);
    }

    /**
     * Tech cards do not need to be shuffled as the player is supposed to pick the card they want
     *
     * @param wb
     * @return
     */
    private List<Tech> getTechsFromExcel(Workbook wb) {
        //Start with level 1 techs
        Sheet sheet = wb.getSheet(SheetName.LEVEL_1_TECH.getName());
        List<Cell> level1Cells = new ArrayList<>();
        sheet.forEach(row -> row.forEach(level1Cells::add));

        List<Tech> level1Techs = level1Cells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(tech -> new Tech(tech.toString(), Tech.LEVEL_1))
                .collect(Collectors.toList());

        sheet = wb.getSheet(SheetName.LEVEL_2_TECH.getName());
        List<Cell> level2Cells = new ArrayList<>();
        sheet.forEach(row -> row.forEach(level2Cells::add));

        List<Tech> level2Techs = level2Cells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(tech -> new Tech(tech.toString(), Tech.LEVEL_2))
                .collect(Collectors.toList());

        sheet = wb.getSheet(SheetName.LEVEL_3_TECH.getName());
        List<Cell> level3Cells = new ArrayList<>();
        sheet.forEach(row -> row.forEach(level3Cells::add));

        List<Tech> level3Techs = level3Cells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(tech -> new Tech(tech.toString(), Tech.LEVEL_3))
                .collect(Collectors.toList());

        sheet = wb.getSheet(SheetName.LEVEL_4_TECH.getName());
        List<Cell> level4Cells = new ArrayList<>();
        sheet.forEach(row -> row.forEach(level4Cells::add));

        List<Tech> level4Techs = level4Cells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(tech -> new Tech(tech.toString(), Tech.LEVEL_4))
                .collect(Collectors.toList());

        List<Tech> allTechs = new ArrayList<>(level1Techs);
        allTechs.addAll(level2Techs);
        allTechs.addAll(level3Techs);
        allTechs.addAll(level4Techs);
        allTechs.add(Tech.SPACE_FLIGHT);
        Collections.sort(allTechs, (o1, o2) -> Integer.valueOf(o1.getLevel()).compareTo(o2.getLevel()));

        return allTechs;
    }

    private void readInfantry(Workbook wb) throws IOException {
        Sheet infantrySheet = wb.getSheet(SheetName.INFANTRY.toString());


        List<Cell> unfilteredCells = new ArrayList<>();
        infantrySheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<Infantry> infantryUnits = unfilteredCells.stream()
                .filter(ItemReader.notEmptyPredicate)
                .filter(ItemReader.notRandomPredicate)
                .filter(ItemReader.rowNotZeroPredicate)
                .filter(ItemReader.columnIndexZeroPredicate)
                .map(cell -> createInfantry(cell.toString()))
                .collect(Collectors.toList());

        Collections.shuffle(infantryUnits);
        infantryList = new LinkedList<>(infantryUnits);
        log.debug(infantryList);
    }

    private void readMounted(Workbook wb) throws IOException {
        Sheet mountedsheet = wb.getSheet(SheetName.MOUNTED.toString());

        List<Cell> unfilteredCells = new ArrayList<>();
        mountedsheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<Mounted> mountedUnits = unfilteredCells.stream()
                .filter(ItemReader.notEmptyPredicate)
                .filter(ItemReader.notRandomPredicate)
                .filter(ItemReader.rowNotZeroPredicate)
                .filter(ItemReader.columnIndexZeroPredicate)
                .map(cell -> createMounted(cell.toString()))
                .collect(Collectors.toList());

        Collections.shuffle(mountedUnits);
        mountedList = new LinkedList<>(mountedUnits);
        log.debug("Mounted units from excel are " + mountedList);
    }

    private void readArtillery(Workbook wb) throws IOException {
        Sheet artillerySheet = wb.getSheet(SheetName.ARTILLERY.toString());

        List<Cell> unfilteredCells = new ArrayList<>();
        artillerySheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<Artillery> artilleryUnits = unfilteredCells.stream()
                .filter(ItemReader.notEmptyPredicate)
                .filter(ItemReader.notRandomPredicate)
                .filter(ItemReader.rowNotZeroPredicate)
                .filter(ItemReader.columnIndexZeroPredicate)
                .map(cell -> createArtillery(cell.toString()))
                .collect(Collectors.toList());

        Collections.shuffle(artilleryUnits);
        artilleryList = new LinkedList<>(artilleryUnits);
        log.debug("Artillery units from excel are " + artilleryList);
    }

    private void readAircraft(Workbook wb) throws IOException {
        Sheet aircraftSheet = wb.getSheet(SheetName.AIRCRAFT.toString());

        List<Cell> unfilteredCells = new ArrayList<>();
        aircraftSheet.forEach(row -> row.forEach(unfilteredCells::add));

        List<Aircraft> aircraftUnits = unfilteredCells.stream()
                .filter(ItemReader.notEmptyPredicate)
                .filter(ItemReader.notRandomPredicate)
                .filter(ItemReader.rowNotZeroPredicate)
                .filter(ItemReader.columnIndexZeroPredicate)
                .map(cell -> createAircraft(cell.toString()))
                .collect(Collectors.toList());

        Collections.shuffle(aircraftUnits);
        aircraftList = new LinkedList<>(aircraftUnits);
        log.debug("Aircraft units from excel are " + aircraftList);
    }

    private LinkedList<? extends Item> getSocialPolicies(Workbook wb) {
        Sheet spSheet = wb.getSheet(SheetName.SOCIAL_POLICY.getName().toString());

        List<Cell> unfilteredSPCells = new ArrayList<>();
        spSheet.forEach(row -> row.forEach(unfilteredSPCells::add));

        List<SocialPolicy> sps = unfilteredSPCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(columnIndexZeroPredicate)
                .map(sp -> new SocialPolicy(sp.toString()))
                .collect(Collectors.toList());

        List<String> descriptionCells = unfilteredSPCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 1)
                .map(Object::toString)
                .collect(Collectors.toList());

        List<String> flipsideCells = unfilteredSPCells.stream()
                .filter(notEmptyPredicate)
                .filter(notRandomPredicate)
                .filter(rowNotZeroPredicate)
                .filter(cell -> cell.getColumnIndex() == 2)
                .map(Object::toString)
                .collect(Collectors.toList());

        //Description should be in the same order as flipside
        for (int i = 0; i < sps.size(); i++) {
            SocialPolicy item = sps.get(i);
            item.setDescription(descriptionCells.get(i));
            item.setFlipside(flipsideCells.get(i));
        }

        Collections.shuffle(sps);
        return new LinkedList<>(sps);
    }
} public class CivUtil {

    public static <T> Stream<T> streamFromIterable(Iterable<T> in) {
        return StreamSupport.stream(in.spliterator(), false);
    }

    public static boolean shouldSendEmailInGame(Playerhand player) {
        //30 min
        int min = 60 * 30;
        if (shouldSend(player.getIfEmailSent(), min)) {
            player.setEmailSent(LocalDateTime.now());
            return true;
        }
        return false;
    }

    public static boolean shouldSendEmail(Player player) {
        //3 hours
        int min = 60 * 180;
        if (shouldSend(player.getIfEmailSent(), min)) {
            player.setEmailSent(LocalDateTime.now());
            return true;
        }
        return false;
    }

    private static boolean shouldSend(Optional<LocalDateTime> emailSentOpt, int timeToWait) {
        if (emailSentOpt.isPresent()) {
            long lastEmailSent = emailSentOpt.get().atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
            long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
            return timeToWait < Math.abs(lastEmailSent - now);
        }
        return true;
    }
} public class SecurityCheck {

    public static boolean hasUserAccess(PBF pbf, String playerId) {
        if (pbf != null) {
            return pbf.getPlayers().stream().anyMatch(p -> p.getPlayerId().equals(playerId));
        }

        return false;
    }
} @Data
@JsonRootName("chat")
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Chat {
    public static final String COL_NAME = "chat";
    public static final String PBFID = "pbfId";

    @Id
    @ObjectId
    private String id;

    private String pbfId;

    @NotNull
    private String username;

    @NotEmpty
    private String message;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime created = LocalDateTime.now();

    @JsonIgnore
    public long getCreatedInMillis() {
        return created.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }
} @Getter
@Setter
@ToString(of = "name")
@JsonTypeName("civ")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "objectType")
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
@EqualsAndHashCode(of = {"name", "description", "type"})
public class Civ implements Item, Image {
    @NotEmpty
    private String name;
    private String type;
    private String description;
    private boolean used;
    private boolean hidden = true;
    private String ownerId; // id of the player which owns this item
    private SheetName sheetName;
    private Tech startingTech;
    private int itemNumber;
    private String image;

    public Civ(String name) {
        this.name = name;
        this.used = false;
        this.hidden = true;
    }

    @Override
    public SheetName getSheetName() {
        return sheetName = SheetName.CIV;
    }

    @Override
    public String revealPublic() {
        return getClass().getSimpleName();
    }

    @Override
    public String revealAll() {
        return getClass().getSimpleName() + " : " + name;
    }

    @Override
    public int compareTo(Spreadsheet o) {
        return getSheetName().compareTo(o.getSheetName());
    }

    @JsonGetter("image")
    @Override
    public String getImage() {
        image = name + PNG;
        return image;
    }
} @JsonRootName("gameLog")
@NoArgsConstructor
@ToString(of = {"privateLog", "publicLog"})
@Data
public class GameLog {
    public static final String COL_NAME = "gamelog";
    private static final String DELIM = " - ";
    @Id
    @ObjectId
    private String id;
    private String privateLog;
    private String publicLog;
    /**
     * Each log belongs to a pbf
     */
    @NotEmpty
    private String pbfId;
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime created = LocalDateTime.now();
    @NotEmpty
    private String username;
    /**
     * If log is from a draw
     */
    private Draw draw;

    @JsonIgnore
    public long getCreatedInMillis() {
        return created.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }

    @JsonIgnore
    public String uniqueItemNumber(int itemNumber) {
        int uniqueNrThreeLetters = Integer.valueOf(("" + Math.abs(username.hashCode())).substring(0, 3));
        return ". Item number #" + (uniqueNrThreeLetters + itemNumber);
    }

    @JsonIgnore
    public void createAndSetLog(LogType logType, int itemNumber) {
        final String ITEM_NUMBER = ". Item number #" + itemNumber;
        final String UNIQUE_ITEM_NUMBER = uniqueItemNumber(itemNumber);
        switch (logType) {
            case ITEM:
                privateLog = username + " drew " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                publicLog = username + " drew " + DELIM + draw.getItem().revealPublic() + ITEM_NUMBER;
                break;
            case BATTLE:
                privateLog = username + " plays " + DELIM + draw.getItem().revealAll();
                publicLog = username + " reveals " + DELIM + draw.getItem().revealPublic();
                break;
            case TRADE_BETWEEN_PLAYERS:
                privateLog = username + " has received " + DELIM + draw.getItem().revealAll();
                publicLog = username + " has received " + DELIM + draw.getItem().revealPublic();
                break;
            case SOCIAL_POLICY:
                privateLog = username + " has chosen " + DELIM + draw.getItem().revealAll() + UNIQUE_ITEM_NUMBER;
                publicLog = username + " has chosen a hidden social policy" + UNIQUE_ITEM_NUMBER;
                break;
            case TECH:
                privateLog = username + " has researched " + DELIM + draw.getItem().revealAll() + UNIQUE_ITEM_NUMBER;
                publicLog = username + " has researched a hidden technology" + UNIQUE_ITEM_NUMBER;
                break;
            case REMOVED_TECH:
                privateLog = username + " has removed " + DELIM + draw.getItem().revealAll() + UNIQUE_ITEM_NUMBER;
                publicLog = username + " has removed a hidden technology" + UNIQUE_ITEM_NUMBER;
                break;
            case DISCARD:
                privateLog = username + " has discarded " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                publicLog = username + " has discarded " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                break;
            case REVEAL:
                if (draw.getItem() instanceof Tech) {
                    privateLog = username + " has revealed " + DELIM + draw.getItem().revealAll() + UNIQUE_ITEM_NUMBER;
                    publicLog = username + " has revealed " + DELIM + draw.getItem().revealAll() + UNIQUE_ITEM_NUMBER;
                } else {
                    privateLog = username + " has revealed " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                    publicLog = username + " has revealed " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                }
                break;
            case UNDO:
                privateLog = username + " has requested undo of " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                publicLog = username + " has requested undo of " + DELIM + draw.getItem().revealAll() + ITEM_NUMBER;
                break;
            case SOT:
                privateLog = username + " has updated start of turn phase";
                publicLog = username + " has updated start of turn phase";
                break;
            case SETUP:
                privateLog = username + " has updated setup phase";
                publicLog = username + " has updated setup phase";
                break;
            case TRADE:
                privateLog = username + " has updated trade phase";
                publicLog = username + " has updated trade phase";
                break;
            case CM:
                privateLog = username + " has updated city management phase";
                publicLog = username + " has updated city management phase";
                break;
            case MOVEMENT:
                privateLog = username + " has updated movement phase";
                publicLog = username + " has updated movement phase";
                break;
            case RESEARCH:
                privateLog = username + " has updated research phase";
                publicLog = username + " has updated research phase";
                break;
        }
    }

    @JsonIgnore
    public boolean hasUndo() {
        return draw != null && draw.getUndo() != null;
    }

    @JsonIgnore
    public boolean hasActiveUndo() {
        return draw != null && draw.getUndo() != null && !draw.getUndo().isDone();
    }

    public enum LogType {
        TRADE_BETWEEN_PLAYERS, BATTLE, ITEM, TECH, REMOVED_TECH, SHUFFLE, DISCARD, WITHDRAW, JOIN, REVEAL,
        UNDO, SOCIAL_POLICY, REMOVED_SOCIAL_POLICY, VOTE, SETUP, SOT, TRADE, CM, MOVEMENT, RESEARCH;
    }
} public enum GameType {
    BASE("Base Game"), FAF("Fame and Fortune"), WAW("Wisdom and Warfare"), DOC("Dawn of Civilization");

    private String label;

    GameType(String name) {
        this.label = name;
    }

    public static Optional<GameType> find(String name) {
        String spacesRemovedName = name.replaceAll("\\s", "");
        Optional<GameType> found = Stream.of(BASE, FAF, WAW, DOC)
                .filter(type -> type.label.replaceAll("\\s", "").equalsIgnoreCase(spacesRemovedName))
                .findFirst();
        if (!found.isPresent()) {
            try {
                return Optional.of(valueOf(name.toUpperCase()));
            } catch (Exception ex) {
                return Optional.empty();
            }
        }
        return found;
    }

    @Override
    public String toString() {
        return label;
    }

} public interface Item extends Spreadsheet, Type, Comparable<Spreadsheet> {

    /**
     * Either the username or pbf name, both of which must be unique *
     */
    public String getName();

    public String getOwnerId();

    public void setOwnerId(String owner);

    public boolean isHidden();

    public void setHidden(boolean hidden);

    public boolean isUsed();

    public String getDescription();

    public int getItemNumber();

    public void setItemNumber(int itemNumber);

} @Data
@JsonRootName(value = "pbf")
@XmlRootElement
@JsonInclude
@JsonIgnoreProperties(ignoreUnknown = true)
//Perhaps use this when the object keep getting changed, otherwise jackson throws exception when it cannot map
public class PBF {
    @JsonIgnore
    public static final String COL_NAME = "pbf";
    @JsonIgnore
    public static final String NAME = "name";

    @ObjectId
    @Id
    private String id;

    @NotBlank
    private String name;
    private GameType type;
    private String mapLink;
    private String assetLink;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime created = LocalDateTime.now();

    private int numOfPlayers;
    private boolean active = true;
    private String winner; //username
    private List<Item> items = Lists.newArrayList();
    private List<Playerhand> players = Lists.newArrayList();
    private List<Tech> techs = Lists.newArrayList();
    private List<SocialPolicy> socialPolicies = new ArrayList<>(8);

    //@JsonSerialize(keyUsing=TurnKeySerializer.class)
    //@JsonDeserialize(keyUsing = TurnKeyDeserializer.class)
    private Map<String, PlayerTurn> publicTurns = new HashMap<>();

    //Will use these to reshuffle items which are discarded and can be drawn again
    private List<Item> discardedItems = new ArrayList<>();

    //If a player leaves, his items and stuff will be put here
    private List<Playerhand> withdrawnPlayers = new ArrayList<>();

    private boolean tournament = false;

    /**
     * Returns the username of the player who is start of turn
     */
    @JsonIgnore
    public String getNameOfUsersTurn() {
        Optional<Playerhand> optional = players.stream()
                .filter(Playerhand::isYourTurn)
                .findFirst();

        if (optional.isPresent()) {
            return optional.get().getUsername();
        }

        return "";
    }

} @Data
@JsonRootName("player")
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Player {
    @JsonIgnore
    public static final String COL_NAME = "player";
    @JsonIgnore
    public static final String USERNAME = "username";
    @JsonIgnore
    public static final String EMAIL = "email";

    @ObjectId
    @Id
    private String id;

    @NotBlank
    //Unique
    private String username;

    @Email
    private String email;

    private boolean disableEmail = false;

    @NotBlank
    private String password;

    private String newPassword;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime emailSent;

    /**
     * Set of unique active games This may be reduntant as it can be calculated by looping through all pbfs.players and finding match.
     */
    private Set<String> gameIds = new HashSet<>();

    @JsonIgnore
    public Optional<LocalDateTime> getIfEmailSent() {
        return Optional.ofNullable(emailSent);
    }
} @Data
@JsonRootName("players")
@NoArgsConstructor
@EqualsAndHashCode(of = {"username", "playerId"})
@JsonIgnoreProperties(ignoreUnknown = true)
public class Playerhand implements Comparable<Playerhand> {
    @NotBlank
    //Can consider using the playerId instead or removing @NotBlank
    private String username;

    @NotBlank
    private String playerId;

    private String email;

    private String color;

    private Civ civilization; //The chosen civilization

    /**
     * Save personal gamenotes
     **/
    private String gamenote;

    //Only one starting player each turn
    /**
     * Determines whos turn it is each round *
     */
    private boolean yourTurn = false;
    /**
     * Determines player order (ie: Player 2)
     */
    private int playernumber;
    private boolean gameCreator = false;

    private List<Item> items = new ArrayList<>();
    private Set<Tech> techsChosen = new TreeSet<>();
    private List<Unit> barbarians = new ArrayList<>(3);
    private List<Unit> battlehand = new ArrayList<>();
    private List<SocialPolicy> socialPolicies = new ArrayList<>();
    /**
     * Private turns, only made public when they are locked
     */
    private Set<PlayerTurn> playerTurns = new TreeSet<>();

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime emailSent;

    @JsonIgnore
    public static String green() {
        return "Green";
    }

    @JsonIgnore
    public static String yellow() {
        return "Yellow";
    }

    @JsonIgnore
    public static String purple() {
        return "Purple";
    }

    @JsonIgnore
    public static String red() {
        return "Red";
    }

    @JsonIgnore
    public static String blue() {
        return "Blue";
    }

    @JsonIgnore
    public Optional<LocalDateTime> getIfEmailSent() {
        return Optional.ofNullable(emailSent);
    }

    @Override
    public int compareTo(Playerhand o) {
        return o.getUsername().compareTo(o.getUsername());
    }
}
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.mongodb.DB;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j;
import no.asgari.civilization.server.application.CivSingleton;
import no.asgari.civilization.server.dto.ChatDTO;
import no.asgari.civilization.server.dto.CivHighscoreDTO;
import no.asgari.civilization.server.dto.CreateNewGameDTO;
import no.asgari.civilization.server.dto.DrawDTO;
import no.asgari.civilization.server.dto.GameDTO;
import no.asgari.civilization.server.dto.GameLogDTO;
import no.asgari.civilization.server.dto.MessageDTO;
import no.asgari.civilization.server.dto.PbfDTO;
import no.asgari.civilization.server.dto.PlayerDTO;
import no.asgari.civilization.server.dto.PlayerHighscoreDTO;
import no.asgari.civilization.server.dto.WinnerDTO;
import no.asgari.civilization.server.email.SendEmail;
import no.asgari.civilization.server.excel.ItemReader;
import no.asgari.civilization.server.misc.CivUtil;
import no.asgari.civilization.server.misc.SecurityCheck;
import no.asgari.civilization.server.model.Chat;
import no.asgari.civilization.server.model.Civ;
import no.asgari.civilization.server.model.GameLog;
import no.asgari.civilization.server.model.GameType;
import no.asgari.civilization.server.model.Item;
import no.asgari.civilization.server.model.PBF;
import no.asgari.civilization.server.model.Player;
import no.asgari.civilization.server.model.Playerhand;
import org.apache.commons.lang3.StringUtils;
import org.mongojack.DBQuery;
import org.mongojack.DBSort;
import org.mongojack.JacksonDBCollection;
import org.mongojack.WriteResult;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URLDecoder;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
/*
 * Copyright (c) 2015 Shervin Asgari
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package no.asgari.civilization.server.action;




@Log4j
public class GameAction extends BaseAction {
    private final JacksonDBCollection<PBF, String> pbfCollection;
    private final JacksonDBCollection<Player, String> playerCollection;
    private final GameLogAction gameLogAction;
    private final JacksonDBCollection<Chat, String> chatCollection;

    public GameAction(DB db) {
        super(db);
        this.playerCollection = JacksonDBCollection.wrap(db.getCollection(Player.COL_NAME), Player.class, String.class);
        this.pbfCollection = JacksonDBCollection.wrap(db.getCollection(PBF.COL_NAME), PBF.class, String.class);
        this.chatCollection = JacksonDBCollection.wrap(db.getCollection(Chat.COL_NAME), Chat.class, String.class);
        this.gameLogAction = new GameLogAction(db);
    }

    /**
     * Creating PbfDTO so to not include every players Ghand and information
     *
     * @param pbf - the PBF
     * @return PbfDto
     */
    private static PbfDTO createPbfDTO(PBF pbf) {
        PbfDTO dto = new PbfDTO();
        dto.setType(pbf.getType());
        dto.setId(pbf.getId());
        dto.setName(pbf.getName());
        dto.setActive(pbf.isActive());
        long created = pbf.getCreated().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        dto.setCreated(created);
        dto.setNumOfPlayers(pbf.getNumOfPlayers());
        dto.setPlayers(pbf.getPlayers().stream()
                .map(p -> createPlayerDTO(p, pbf.getId()))
                .collect(toList()));
        dto.setNameOfUsersTurn(pbf.getNameOfUsersTurn());
        return dto;
    }

    private static PlayerDTO createPlayerDTO(Playerhand player, String pbfId) {
        PlayerDTO dto = new PlayerDTO();
        dto.setUsername(player.getUsername());
        dto.setPlayerId(player.getPlayerId());
        dto.setPbfId(pbfId);
        return dto;
    }

    private static Playerhand createPlayerHand(Player player, String color, boolean gameCreator) {
        Playerhand playerhand = new Playerhand();
        playerhand.setUsername(player.getUsername());
        playerhand.setPlayerId(player.getId());
        playerhand.setYourTurn(false);
        playerhand.setColor(color);
        playerhand.setGameCreator(gameCreator);
        playerhand.setEmail(player.getEmail());
        return playerhand;
    }

    public String createNewGame(CreateNewGameDTO dto, String playerId) {