@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) {
        PBF pbf = new PBF();
        pbf.setName(dto.getName());
        pbf.setType(dto.getType());
        pbf.setNumOfPlayers(dto.getNumOfPlayers());
        ItemReader itemReader = new ItemReader();
        readItemFromExcel(dto.getType(), itemReader);

        pbf.getItems().addAll(itemReader.shuffledCivs);
        pbf.getItems().addAll(itemReader.shuffledCultureI);
        pbf.getItems().addAll(itemReader.shuffledCultureII);
        pbf.getItems().addAll(itemReader.shuffledCultureIII);
        pbf.getItems().addAll(itemReader.shuffledGPs);
        pbf.getItems().addAll(itemReader.shuffledHuts);
        pbf.getItems().addAll(itemReader.shuffledVillages);
        pbf.getItems().addAll(itemReader.shuffledTiles);
        pbf.getItems().addAll(itemReader.shuffledCityStates);
        pbf.getItems().addAll(itemReader.ancientWonders);
        pbf.getItems().addAll(itemReader.medievalWonders);
        pbf.getItems().addAll(itemReader.modernWonders);
        pbf.getItems().addAll(itemReader.mountedList);
        pbf.getItems().addAll(itemReader.aircraftList);
        pbf.getItems().addAll(itemReader.artilleryList);
        pbf.getItems().addAll(itemReader.infantryList);
        pbf.getTechs().addAll(itemReader.allTechs);
        pbf.getSocialPolicies().addAll(itemReader.socialPolicies);

        Collections.shuffle(pbf.getItems(), new Random(System.nanoTime()));
        Collections.shuffle(pbf.getTechs(), new Random(System.nanoTime()));
        Collections.shuffle(pbf.getSocialPolicies(), new Random(System.nanoTime()));

        pbf.getItems().forEach(it -> it.setItemNumber(ItemReader.itemCounter.incrementAndGet()));
        pbf.getTechs().forEach(it -> it.setItemNumber(ItemReader.itemCounter.incrementAndGet()));
        pbf.getSocialPolicies().forEach(it -> it.setItemNumber(ItemReader.itemCounter.incrementAndGet()));

        WriteResult<PBF, String> pbfInsert = pbfCollection.insert(pbf);
        pbf.setId(pbfInsert.getSavedId());
        log.info("PBF game created with id " + pbfInsert.getSavedId());
        joinGame(pbf, playerId, Optional.of(dto.getColor()), true);

        //Do this in a new thread
        Thread thread = new Thread(() -> {
            playerCollection.find().toArray().stream()
                    .filter(p -> !p.isDisableEmail())
                    .filter(CivUtil::shouldSendEmail)
                    .forEach(p -> {
                        SendEmail.sendMessage(p.getEmail(), "New Civilization game created",
                                "A new game by the name " + pbf.getName() + " was just created! Visit " + SendEmail.URL + " to join the game.", p.getId());
                        playerCollection.updateById(p.getId(), p);
                    });
        });
        thread.start();
        return pbf.getId();
    }

    private void readItemFromExcel(GameType gameType, ItemReader itemReader) {
        try {
            itemReader.readItemsFromExcel(gameType);
            if (!CivSingleton.instance().itemsCache().containsKey(gameType)) {
                CivSingleton.instance().itemsCache().put(gameType, itemReader);
            }
        } catch (IOException e) {
            log.error("Couldn't read Excel document " + e.getMessage(), e);
        }
    }

    /**
     * Returns all games sorted on active first
     *
     * @return
     */
    public List<PbfDTO> getAllGames() {
        List<PBF> pbfs = pbfCollection.find().toArray();
        return pbfs.stream()
                .map(GameAction::createPbfDTO)
                .sorted((o1, o2) -> {
                    int v = Boolean.valueOf(o1.isActive()).compareTo(o2.isActive());
                    if (v != 0) return v;
                    return Long.valueOf(o1.getCreated()).compareTo(o2.getCreated());
                })
                .collect(toList());
    }

    public void joinGame(String pbfId, Player player, Optional<String> colorOpt) {
        PBF pbf = pbfCollection.findOneById(pbfId);

        joinGame(pbf, player.getId(), colorOpt, false);

        Thread thread = new Thread(() -> {
            pbf.getPlayers().stream()
                    .filter(p -> !p.getPlayerId().equals(player.getId()))
                    .forEach(p -> SendEmail.sendMessage(p.getEmail(), "Game update", player.getUsername() + " joined " + pbf.getName() + ". Go to " + SendEmail.URL + " to find out who!", p.getPlayerId()));
        });
        thread.start();
    }

    /**
     * Joins a game. If it is full it will throw exception
     */
    private void joinGame(PBF pbf, String playerId, Optional<String> colorOpt, boolean gameCreator) {
        if (pbf.getNumOfPlayers() == pbf.getPlayers().size()) {
            log.warn("Cannot join the game. Its full");
            Response badReq = Response.status(Response.Status.BAD_REQUEST)
                    .entity(new MessageDTO("Cannot join the game. Its full!"))
                    .build();
            throw new WebApplicationException(badReq);
        }

        Player player = playerCollection.findOneById(playerId);

        boolean playerAlreadyJoined = pbf.getPlayers().stream()
                .anyMatch(p -> p.getPlayerId().equals(player.getId()));
        if (playerAlreadyJoined) {
            log.warn("Cannot join the game. Player has already joined it");
            Response badReq = Response.status(Response.Status.BAD_REQUEST)
                    .entity(new MessageDTO("Cannot join the game. You have already joined!"))
                    .build();
            throw new WebApplicationException(badReq);
        }

        player.getGameIds().add(pbf.getId());
        playerCollection.updateById(player.getId(), player);
        Playerhand playerhand;
        if (!pbf.getWithdrawnPlayers().isEmpty()) {
            playerhand = pbf.getWithdrawnPlayers().remove(0);
            boolean updated = gameLogAction.updateGameLog(pbf.getId(), playerhand.getUsername(), player.getUsername());
            log.info("Managed to update gameLog: " + updated);
            playerhand.setEmail(player.getEmail());
            playerhand.setPlayerId(player.getId());
            playerhand.setUsername(player.getUsername());
        } else {
            String color = colorOpt.orElse(chooseColorForPlayer(pbf));
            playerhand = createPlayerHand(player, color, gameCreator);
        }
        if (!pbf.getPlayers().contains(playerhand)) {
            createInfoLog(pbf.getId(), playerhand.getUsername() + " joined the game and is playing color " + playerhand.getColor());
            pbf.getPlayers().add(playerhand);
        }
        pbf = startIfAllPlayers(pbf);
        pbfCollection.updateById(pbf.getId(), pbf);
    }

    private String chooseColorForPlayer(PBF pbf) {
        if (pbf.getPlayers().isEmpty()) {
            return Playerhand.green();
        }

        Set<String> allColors = Sets.newHashSet(Playerhand.green(), Playerhand.purple(), Playerhand.blue(), Playerhand.yellow(), Playerhand.red());

        Set<String> colors = pbf.getPlayers().stream()
                .map(Playerhand::getColor)
                .collect(Collectors.toSet());

        Sets.SetView<String> difference = Sets.difference(allColors, colors);
        return difference.iterator().next();
    }

    public List<PlayerDTO> getAllPlayers(String pbfId) {
        Preconditions.checkNotNull(pbfId);
        PBF pbf = pbfCollection.findOneById(pbfId);
        return pbf.getPlayers().stream()
                .map(p -> createPlayerDTO(p, pbf.getId()))
                .sorted((o1, o2) -> o1.getUsername().compareTo(o2.getUsername()))
                .collect(toList());
    }

    private PBF startIfAllPlayers(PBF pbf) {
        if (pbf.getPlayers().stream().anyMatch(Playerhand::isYourTurn)) {
            return pbf;
        }
        final int numOfPlayersNeeded = pbf.getNumOfPlayers();
        if (numOfPlayersNeeded == pbf.getPlayers().size()) {
            Collections.shuffle(pbf.getPlayers());
            Playerhand firstplayer = pbf.getPlayers().get(0);
            firstplayer.setYourTurn(true);
            firstplayer.setPlayernumber(1);

            createInfoLog(pbf.getId(), "Game has now started. Good luck, and have fun!");

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < pbf.getPlayers().size(); i++) {
                Playerhand player = pbf.getPlayers().get(i);
                player.setPlayernumber(i + 1);
                sb.append(getNameForPlayerNumber(i) + " player is " + player.getUsername() + ". ");
            }
            createInfoLog(pbf.getId(), sb.toString());
        }
        return pbf;
    }

    public GameDTO mapGameDTO(PBF pbf, Player player) {
        Preconditions.checkNotNull(pbf);
        //Set common stuff
        GameDTO dto = new GameDTO();
        long created = pbf.getCreated().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        dto.setCreated(created);
        dto.setId(pbf.getId());
        dto.setType(pbf.getType());
        dto.setName(pbf.getName());
        dto.setWhosTurnIsIt(pbf.getNameOfUsersTurn());
        dto.setActive(pbf.isActive());
        dto.setMapLink(pbf.getMapLink());
        dto.setAssetLink(pbf.getAssetLink());

        //Set logs
        List<GameLog> allPublicLogs = gameLogAction.getGameLogs(pbf.getId());
        List<GameLogDTO> publicGamelogDTOs = allPublicLogs.stream()
                .filter(log -> !Strings.isNullOrEmpty(log.getPublicLog()))
                .map(log -> new GameLogDTO(log.getId(), log.getPublicLog(), log.getCreatedInMillis(), new DrawDTO(log.getDraw())))
                .sorted(Comparator.comparing(GameLogDTO::getCreated))
                .collect(toList());
        dto.setPublicLogs(publicGamelogDTOs);

        //Set private player info if correct player is loggedIn.
        if (player != null && !Strings.isNullOrEmpty(player.getUsername()) && !Strings.isNullOrEmpty(player.getId())) {
            //Get all the private player stuff
            Optional<Playerhand> playerhand = pbf.getPlayers()
                    .stream()
                    .filter(p -> p.getPlayerId().equals(player.getId()))
                    .findFirst();

            if (playerhand.isPresent()) {
                List<GameLog> allPrivateLogs = gameLogAction.getGameLogsBelongingToPlayer(pbf.getId(), playerhand.get().getUsername());
                List<GameLogDTO> privateGamelogDTOs = allPrivateLogs.stream()
                        .filter(log -> !Strings.isNullOrEmpty(log.getPrivateLog()))
                        .map(log -> new GameLogDTO(log.getId(), log.getPrivateLog(), log.getCreatedInMillis(), new DrawDTO(log.getDraw())))
                        .sorted(Comparator.comparing(GameLogDTO::getCreated))
                        .collect(toList());

                dto.setPlayer(playerhand.get());
                dto.setPrivateLogs(privateGamelogDTOs);
            }
        }
        dto.setRevealedItems(getAllRevealedItems(pbf));
        return dto;
    }

    public boolean withdrawFromGame(String pbfId, String playerId) {
        PBF pbf = pbfCollection.findOneById(pbfId);

        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.warn("User with id " + playerId + " is not player of this game, and cannot withdraw");
            Response badReq = Response.status(Response.Status.FORBIDDEN)
                    .entity(new MessageDTO("User is not player of this game, and cannot withdraw"))
                    .build();
            throw new WebApplicationException(badReq);
        }

        Iterator<Playerhand> iterator = pbf.getPlayers().iterator();
        while (iterator.hasNext()) {
            Playerhand playerhand = iterator.next();
            if (playerhand.getPlayerId().equals(playerId)) {
                if (playerhand.isGameCreator()) {
                    Optional<Playerhand> optionalNextPlayer = getRandomPlayerExcept(pbf.getPlayers(), playerhand.getUsername());
                    if (optionalNextPlayer.isPresent()) {
                        Playerhand nextPlayer = optionalNextPlayer.get();
                        nextPlayer.setGameCreator(true);
                        gameLogAction.createCommonPrivatePublicLog("Is now game creator", pbfId, nextPlayer.getPlayerId());
                    } else {
                        Response badReq = Response.status(Response.Status.FORBIDDEN)
                                .entity(new MessageDTO("As game creator, you must end game not withdraw from it"))
                                .build();
                        throw new WebApplicationException(badReq);
                    }
                }
                pbf.getWithdrawnPlayers().add(playerhand);
                iterator.remove();
                gameLogAction.createCommonPublicLog("withdrew from game", pbfId, playerId);
                //TODO remove from PlayerCollection also
                pbfCollection.updateById(pbf.getId(), pbf);
                return true;
            }
        }

        return false;
    }

    private Optional<Playerhand> getRandomPlayerExcept(List<Playerhand> players, String username) {
        return players.stream().filter(p -> !p.getUsername().equals(username)).findAny();
    }

    @SneakyThrows
    public Chat chat(String pbfId, String message, String username) {
        Chat chat = new Chat();
        chat.setPbfId(pbfId);
        chat.setMessage(URLDecoder.decode(message, "UTF-8"));
        chat.setUsername(username);
        String id = chatCollection.insert(chat).getSavedId();
        chat.setId(id);

        if (pbfId != null) {
            PBF pbf = findPBFById(pbfId);

            if (StringUtils.isNotBlank(message)) {
                pbf.getPlayers()
                        .stream()
                        .filter(p -> !p.getUsername().equals(username))
                        .filter(CivUtil::shouldSendEmailInGame)
                        .forEach(p -> {
                                    SendEmail.sendMessage(p.getEmail(), "New Chat", username + " wrote in the chat: " + chat.getMessage()
                                            + ".\nLogin to " + SendEmail.gamelink(pbfId) + " to see the chat", p.getPlayerId());
                                    pbfCollection.updateById(pbfId, pbf);
                                }
                        );
            }
        }

        return chat;
    }

    public List<ChatDTO> getChat(String pbfId) {
        Preconditions.checkNotNull(pbfId);
        List<Chat> chats = chatCollection.find(DBQuery.is("pbfId", pbfId)).sort(DBSort.desc("created")).toArray();
        if (chats == null) {
            return new ArrayList<>();
        }
        PBF pbf = findPBFById(pbfId);
        Map<String, String> colorMap = pbf.getPlayers().stream()
                .collect(Collectors.toMap(Playerhand::getUsername, (playerhand) -> {
                    return (playerhand.getColor() != null) ? playerhand.getColor() : "";
                }));

        List<ChatDTO> chatDTOs = new ArrayList<>(chats.size());
        for (Chat c : chats) {
            chatDTOs.add(new ChatDTO(c.getId(), c.getPbfId(), c.getUsername(), c.getMessage(), colorMap.get(c.getUsername()), c.getCreatedInMillis()));
        }

        //Sort newest date first
        chatDTOs.sort((o1, o2) -> -Long.valueOf(o1.getCreated()).compareTo(o2.getCreated()));
        return chatDTOs;
    }

    public void endGame(String pbfId, Player player, String winner) {
        PBF pbf = pbfCollection.findOneById(pbfId);
        if (!"admin".equals(player.getUsername())) {
            Playerhand playerhand = getPlayerhandByPlayerId(player.getId(), pbf);
            //Only game creator can end game
            if (!playerhand.isGameCreator()) {
                Response response = Response.status(Response.Status.FORBIDDEN)
                        .entity(new MessageDTO("Only game creator can end game"))
                        .build();
                throw new WebApplicationException(response);
            }
        }

        if (!Strings.isNullOrEmpty(winner)) {
            if (!pbf.getPlayers().stream()
                    .anyMatch(p -> p.getUsername().equals(winner))) {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
            createInfoLog(pbfId, winner + " won the game! Congratulations!");
            pbf.setWinner(winner);
        }

        pbf.setActive(false);
        createInfoLog(pbfId, player.getUsername() + " Ended this game");
        createInfoLog(pbfId, "Thank you for playing! Please donate if you liked this game!");
        pbfCollection.updateById(pbfId, pbf);

        Thread thread = new Thread(() -> {
            pbf.getPlayers().forEach(p -> SendEmail.sendMessage(p.getEmail(), "Game ended", pbf.getName() + " has ended. I hope you enjoyed playing.\n" +
                    "If you like this game, please consider donating. You can find the link at the bottom of the site. It will help keep the lights on, and continue adding more features!" +
                    "\n\nBest regards Shervin Asgari aka Cash", p.getPlayerId()));
        });
        thread.start();
    }

    /**
     * Will return the id from the google presentation
     */
    @SneakyThrows
    public String addMapLink(String pbfId, String linkEncoded, String playerId) {
        String link = URLDecoder.decode(linkEncoded, "UTF-8");
        if (link.matches("(?i)^https://docs\\.google\\.com/presentation/d/.*$")) {
            String removedPart = link.replace("https://docs.google.com/presentation/d/", "");
            String id = removedPart.split("/")[0];
            log.info("Id from google presentation is: " + id);

            PBF pbf = pbfCollection.findOneById(pbfId);

            pbf.setMapLink(id);
            pbfCollection.updateById(pbfId, pbf);
            Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
            createInfoLog(pbfId, playerhand.getUsername() + " Added map link");
            return id;
        }
        return null;
    }

    /**
     * Will return the id from the google presentation
     */
    @SneakyThrows
    public String addAssetLink(String pbfId, String linkEncoded, String playerId) {
        String link = URLDecoder.decode(linkEncoded, "UTF-8");
        if (link.matches("(?i)^https://docs\\.google\\.com/spreadsheets/d/.*$")) {
            String removedPart = link.replace("https://docs.google.com/spreadsheets/d/", "");
            String id = removedPart.split("/")[0];
            log.info("Id from google presentation is: " + id);

            PBF pbf = pbfCollection.findOneById(pbfId);
            pbf.setAssetLink(id);
            pbfCollection.updateById(pbfId, pbf);
            Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
            createInfoLog(pbfId, playerhand.getUsername() + " Added asset link");
            return id;
        }
        return null;
    }

    public void changeUserFromExistingGame(String gameid, String oldUsername, String newUsername) {
        Preconditions.checkNotNull(gameid);
        Preconditions.checkNotNull(oldUsername);
        Preconditions.checkNotNull(newUsername);

        PBF pbf = pbfCollection.findOneById(gameid);
        Player toPlayer = playerCollection.find(DBQuery.is("username", newUsername)).toArray(1).get(0);

        //Find all instance of ownerid, and replace with newUsername
        Playerhand playerhandToReplace = pbf.getPlayers().stream().filter(p -> p.getUsername().equals(oldUsername)).findFirst().orElseThrow(PlayerAction::cannotFindPlayer);

        playerhandToReplace.setUsername(newUsername);
        playerhandToReplace.setPlayerId(toPlayer.getId());
        playerhandToReplace.setEmail(toPlayer.getEmail());

        playerhandToReplace.getBarbarians().forEach(b -> b.setOwnerId(toPlayer.getId()));
        playerhandToReplace.getBattlehand().forEach(b -> b.setOwnerId(toPlayer.getId()));
        playerhandToReplace.getTechsChosen().forEach(b -> b.setOwnerId(toPlayer.getId()));
        playerhandToReplace.getItems().forEach(b -> b.setOwnerId(toPlayer.getId()));

        pbfCollection.updateById(pbf.getId(), pbf);
        createInfoLog(pbf.getId(), newUsername + " is now playing instead of " + oldUsername);
        SendEmail.sendMessage(playerhandToReplace.getEmail(), "You are now playing in " + pbf.getName(), "Please log in to http://playciv.com and start playing!", playerhandToReplace.getPlayerId());
    }

    public boolean deleteGame(String gameid) {
        Preconditions.checkNotNull(gameid);

        final PBF pbf = findPBFById(gameid);
        WriteResult<PBF, String> writeResult = pbfCollection.removeById(gameid);
        log.warn("Managed to delete game: " + Strings.isNullOrEmpty(writeResult.getWriteResult().toString()));

        List<Player> playerList = playerCollection.find().toArray().stream()
                .filter(p -> p.getGameIds().contains(gameid))
                .collect(toList());

        playerList.forEach(player -> {
            log.info("Deleting game from " + player.getUsername() + "s collection also");
            player.getGameIds().remove(gameid);
            SendEmail.sendMessage(player.getEmail(), "Game deleted", "Your game " + pbf.getName() + " was deleted by the admin. " +
                    "If this was incorrect, please contact the admin.", player.getId());
            playerCollection.save(player);
        });

        return true;
    }

    public void sendMailToAll(String msg) {
        playerCollection.find().toArray()
                .parallelStream()
                .filter(p -> !p.isDisableEmail())
                .forEach(player -> {
                    SendEmail.sendMessage(player.getEmail(), "Message from cash at playciv.com",
                            "Hello " + player.getUsername() +
                                    "\n" + msg, player.getId());
                });
    }

    /**
     * Gets public chat which is 1 week old and maximum 50 entries, sorted on created
     */
    public List<ChatDTO> getPublicChat() {
        return chatCollection.find(DBQuery.notExists("pbfId")).sort(DBSort.desc("created")).toArray()
                .stream()
                .filter(c -> c.getCreated().isAfter(LocalDateTime.now().minusWeeks(2)))
                .sorted((a, b) -> a.getCreated().compareTo(b.getCreated()))
                .map(c -> new ChatDTO(c.getUsername(), c.getMessage(), c.getCreatedInMillis()))
                .limit(50)
                .collect(toList());
    }

    public boolean disableEmailForPlayer(String playerId) {
        Preconditions.checkNotNull(playerId);
        Player player = playerCollection.findOneById(playerId);
        if (player != null) {
            log.warn("Player " + player.getEmail() + " no longer wants email");
            player.setDisableEmail(true);
            playerCollection.updateById(playerId, player);
            return true;
        }
        return false;
    }

    public boolean startEmailForPlayer(String playerId) {
        Preconditions.checkNotNull(playerId);
        Player player = playerCollection.findOneById(playerId);
        if (player != null) {
            log.warn("Player " + player.getEmail() + " no longer wants email");
            player.setDisableEmail(false);
            playerCollection.updateById(playerId, player);
            return true;
        }
        return false;
    }

    public void takeTurn(String gameid, String fromUsername) {
        PBF pbf = findPBFById(gameid);
        if (pbf != null) {
            Optional<Playerhand> player = pbf.getPlayers().stream().filter(p -> p.getUsername().equals(fromUsername)).findFirst();

            if (player.isPresent()) {
                Playerhand playerTurn = pbf.getPlayers().stream().filter(Playerhand::isYourTurn).findFirst().get();
                playerTurn.setYourTurn(false);

                player.get().setYourTurn(true);
                pbfCollection.updateById(gameid, pbf);
                return;
            }
            throw new BadRequestException("Player not found");
        }
        throw new BadRequestException("Game not found");

    }

    public CivHighscoreDTO getCivHighscore() {
        CivHighscoreDTO dto = new CivHighscoreDTO();
        ListMultimap<String, Integer> civWinnersByNumberOfPlayers = ArrayListMultimap.create();
        ListMultimap<String, Integer> twoPlayerWinner = ArrayListMultimap.create();
        ListMultimap<String, Integer> threePlayerWinner = ArrayListMultimap.create();
        ListMultimap<String, Integer> fourPlayerWinner = ArrayListMultimap.create();
        ListMultimap<String, Integer> fivePlayerWinner = ArrayListMultimap.create();
        List<Player> allPlayers = playerCollection.find().toArray();
        dto.setTotalNumberOfPlayers(allPlayers.size());

        List<PBF> finishedGames = pbfCollection.find().toArray().stream()
                .filter(pbf -> !pbf.isActive())
                .filter(pbf -> !Strings.isNullOrEmpty(pbf.getWinner()))
                .filter(pbf -> pbf.getPlayers().stream()
                        .allMatch(p -> p.getCivilization() != null))
                .collect(toList());
        dto.setTotalNumberOfGames(finishedGames.size());

        finishedGames.forEach(pbf -> {
            String winningCiv = pbf.getPlayers().stream()
                    .filter(p -> p.getUsername().equals(pbf.getWinner()))
                    .map(p -> p.getCivilization().getName())
                    .findFirst().orElse("");


            civWinnersByNumberOfPlayers.put(winningCiv, pbf.getNumOfPlayers());

            switch (pbf.getNumOfPlayers()) {
                case 2:
                    twoPlayerWinner.put(winningCiv, 2);
                    break;
                case 3:
                    threePlayerWinner.put(winningCiv, 3);
                    break;
                case 4:
                    fourPlayerWinner.put(winningCiv, 4);
                    break;
                case 5:
                    fivePlayerWinner.put(winningCiv, 5);
                    break;
            }

        });
        dto.setWinners(getAllCivWinners(civWinnersByNumberOfPlayers, finishedGames));
        dto.setFiveWinners(getCivWinners(fivePlayerWinner, finishedGames, 5));
        dto.setFourWinners(getCivWinners(fourPlayerWinner, finishedGames, 4));
        dto.setThreeWinners(getCivWinners(threePlayerWinner, finishedGames, 3));
        dto.setTwoWinners(getCivWinners(twoPlayerWinner, finishedGames, 2));

        dto.setFivePlayerGamesTotal(fivePlayerWinner.size());
        //dto.setFourPlayerGamesTotal(finishedGames.stream().filter(pbf -> pbf.getNumOfPlayers() == 4).count());
        dto.setFourPlayerGamesTotal(fourPlayerWinner.size());
        dto.setThreePlayerGamesTotal(threePlayerWinner.size());
        dto.setTwoPlayerGamesTotal(twoPlayerWinner.size());
        return dto;
    }

    public PlayerHighscoreDTO getPlayerHighScore() {
        ListMultimap<String, Integer> winnersByNumOfPlayers = ArrayListMultimap.create();
        PlayerHighscoreDTO dto = new PlayerHighscoreDTO();
        List<Player> allPlayers = playerCollection.find().toArray();
        dto.setTotalNumberOfPlayers(allPlayers.size());
        //key == username, value = num of players

        List<PBF> finishedGames = pbfCollection.find().toArray().stream()
                .filter(pbf -> !pbf.isActive())
                .filter(pbf -> !Strings.isNullOrEmpty(pbf.getWinner()))
                .collect(toList());
        dto.setTotalNumberOfGames(finishedGames.size());

        finishedGames.forEach(pbf -> winnersByNumOfPlayers.put(pbf.getWinner(), pbf.getNumOfPlayers()));
        dto.setWinners(getAllWinners(winnersByNumOfPlayers, allPlayers, finishedGames));
        dto.setFiveWinners(getWinners(winnersByNumOfPlayers, finishedGames, 5));
        dto.setFourWinners(getWinners(winnersByNumOfPlayers, finishedGames, 4));
        dto.setThreeWinners(getWinners(winnersByNumOfPlayers, finishedGames, 3));
        dto.setTwoWinners(getWinners(winnersByNumOfPlayers, finishedGames, 2));

        dto.setFivePlayerGamesTotal(finishedGames.stream().filter(pbf -> pbf.getNumOfPlayers() == 5).count());
        dto.setFourPlayerGamesTotal(finishedGames.stream().filter(pbf -> pbf.getNumOfPlayers() == 4).count());
        dto.setThreePlayerGamesTotal(finishedGames.stream().filter(pbf -> pbf.getNumOfPlayers() == 3).count());
        dto.setTwoPlayerGamesTotal(finishedGames.stream().filter(pbf -> pbf.getNumOfPlayers() == 2).count());
        return dto;
    }

    private List<WinnerDTO> getAllCivWinners(ListMultimap<String, Integer> winnersByNumOfCiv, List<PBF> finishedGames) {
        Map<String, Long> attemptsPerCivAllGames = finishedGames.stream()
                .flatMap(pbf -> pbf.getPlayers().stream())
                .map(Playerhand::getCivilization)
                .map(Civ::getName)
                .collect(Collectors.groupingBy(e -> e, Collectors.counting()));

        return winnersByNumOfCiv.keySet().stream()
                .map(civ -> {
                    Long attempts = attemptsPerCivAllGames.get(civ);
                    return new WinnerDTO(civ, winnersByNumOfCiv.get(civ).size(), attempts == null ? 0L : attempts);
                })
                .sorted(Comparator.reverseOrder())
                .distinct()
                .collect(toList());
    }


    private List<WinnerDTO> getAllWinners(ListMultimap<String, Integer> winnersByNumOfPlayers, List<Player> allPlayers, List<PBF> finishedGames) {
        Map<String, Long> attemptsPerUserAllGames = finishedGames.stream()
                .flatMap(pbf -> pbf.getPlayers().stream())
                .map(Playerhand::getUsername)
                .collect(Collectors.groupingBy(e -> e, Collectors.counting()));


        List<WinnerDTO> filteredPlayers = allPlayers.stream()
                .filter(p -> !winnersByNumOfPlayers.containsKey(p.getUsername()) && p.getUsername() != null)
                .map(p -> {
                    long attempts = attemptsPerUserAllGames.get(p.getUsername()) == null ? 0L : attemptsPerUserAllGames.get(p.getUsername());
                    return new WinnerDTO(p.getUsername(), 0, attempts);
                })
                .collect(toList());


        List<WinnerDTO> allWinners = winnersByNumOfPlayers.keySet().stream()
                .map(user -> {
                    Long attempts = attemptsPerUserAllGames.get(user);
                    return new WinnerDTO(user, winnersByNumOfPlayers.get(user).size(), attempts == null ? 0L : attempts);
                })
                .distinct()
                .collect(toList());

        allWinners.addAll(filteredPlayers);
        Collections.sort(allWinners);
        Collections.reverse(allWinners);
        return allWinners;
    }

    /**
     * @param civWinners    - List<WinnerCiv, num of players>
     * @param finishedGames
     * @param numOfPlayers
     * @return
     */
    private List<WinnerDTO> getCivWinners(ListMultimap<String, Integer> civWinners, List<PBF> finishedGames, int numOfPlayers) {
        return finishedGames.stream()
                .filter(pbf -> pbf.getNumOfPlayers() == numOfPlayers)
                .map(p -> {
                    String winnerCiv = p.getPlayers().stream()
                            .filter(playerhand -> playerhand.getUsername().equals(p.getWinner()))
                            .map(Playerhand::getCivilization)
                            .map(Civ::getName)
                            .findFirst().orElse("");
                    long attempts = findAllGamesCivHasPlayed(finishedGames, winnerCiv, numOfPlayers);
                    int totalWins = civWinners.get(winnerCiv).size();
                    return new WinnerDTO(winnerCiv, totalWins, attempts);
                })
                .sorted((Comparator.reverseOrder()))
                .distinct()
                .collect(toList());
    }

    private long findAllGamesCivHasPlayed(List<PBF> finishedGames, String winnerCiv, int numOfPlayers) {
        return finishedGames.stream()
                .filter(pbf -> pbf.getNumOfPlayers() == numOfPlayers)
                .filter(pbf -> pbf.getPlayers().stream().anyMatch(p -> p.getCivilization().getName().equals(winnerCiv)))
                .count();
    }

    private List<WinnerDTO> getWinners(ListMultimap<String, Integer> winnersByNumOfPlayers, List<PBF> finishedGames, int numOfPlayers) {
        Map<String, Long> attemptsPerUser = attemptsPerUser(finishedGames, numOfPlayers);
        return finishedGames.stream()
                .filter(pbf -> pbf.getNumOfPlayers() == numOfPlayers)
                .flatMap(pbf -> pbf.getPlayers().stream())
                .distinct()
                //.filter(p -> !winnersByNumOfPlayers.containsKey(p.getUsername()) && p.getUsername() != null)
                .map(p -> {
                    List<Integer> winner = winnersByNumOfPlayers.get(p.getUsername());
                    int totalWins = 0;
                    long attempts = attemptsPerUser.get(p.getUsername());
                    if (winner != null && !winner.isEmpty()) {
                        totalWins = (int) winner
                                .stream()
                                .filter(num -> num == numOfPlayers)
                                .count();
                    }
                    return new WinnerDTO(p.getUsername(), totalWins, attempts);
                })
                .sorted(((o1, o2) -> o2.compareTo(o1)))
                .collect(toList());
    }

    private Map<String, Long> attemptsPerUser(List<PBF> finishedGames, int numOfPlayers) {
        return finishedGames.stream()
                .filter(pbf -> pbf.getNumOfPlayers() == numOfPlayers)
                .flatMap(pbf -> pbf.getPlayers().stream())
                .map(Playerhand::getUsername)
                .collect(Collectors.groupingBy(e -> e, Collectors.counting()));
    }


    private List<Item> getAllRevealedItems(PBF pbf) {
        //Had to have comparator inside sort, otherwise weird exception
        Stream<Item> discardedStream = pbf.getDiscardedItems().stream()
                .sorted((o1, o2) -> o1.getSheetName().compareTo(o2.getSheetName()));

        Stream<Item> playerStream = pbf.getPlayers().stream()
                .flatMap(p -> p.getItems().stream())
                .filter(it -> !it.isHidden())
                .sorted((o1, o2) -> o1.getSheetName().compareTo(o2.getSheetName()));

        Stream<Item> concatedStream = Stream.concat(discardedStream, playerStream);
        return concatedStream.collect(toList());
    }

    private String getNameForPlayerNumber(int nr) {
        switch (nr) {
            case 0:
                return "First";
            case 1:
                return "Second";
            case 2:
                return "Third";
            case 3:
                return "Fourth";
            case 4:
                return "Fifth";
            case 5:
                return "Sixth";
        }

        return "";
    }
} @Log4j
public class GameLogAction {
    private final JacksonDBCollection<GameLog, String> gameLogCollection;
    private final JacksonDBCollection<Player, String> playerCollection;

    public GameLogAction(DB db) {
        this.gameLogCollection = JacksonDBCollection.wrap(db.getCollection(GameLog.COL_NAME), GameLog.class, String.class);
        this.playerCollection = JacksonDBCollection.wrap(db.getCollection(Player.COL_NAME), Player.class, String.class);
    }

    String save(@NotNull @Valid GameLog gameLog) {
        Preconditions.checkNotNull(gameLog);

        WriteResult<GameLog, String> insert = this.gameLogCollection.insert(gameLog);
        log.debug("Saved Gamelog with _id " + insert.getSavedId());
        return insert.getSavedId();
    }

    public GameLog createGameLog(Draw draw, GameLog.LogType logType) {
        GameLog pl = new GameLog();
        pl.setDraw(draw);
        pl.setPbfId(draw.getPbfId());
        try {
            pl.setUsername(CivSingleton.instance().playerCache().get(draw.getPlayerId()));
        } catch (ExecutionException e) {
            log.error("Couldn't retrieve username from cache");
            pl.setUsername(getUsernameFromPlayerId(draw.getPlayerId()));
        }
        pl.createAndSetLog(logType, draw.getItem() != null ? draw.getItem().getItemNumber() : 0);
        pl.setId(save(pl));
        return pl;
    }

    public GameLog createGameLog(Item item, String pbfId, GameLog.LogType logType) {
        return createGameLog(item, pbfId, logType, item.getOwnerId());
    }

    public GameLog createGameLog(Item item, String pbfId, GameLog.LogType logType, String playerId) {
        GameLog pl = new GameLog();
        Draw<Item> draw = new Draw<>(pbfId, item.getOwnerId());
        draw.setItem(item);
        pl.setDraw(draw);
        pl.setPbfId(pbfId);
        try {
            pl.setUsername(CivSingleton.instance().playerCache().get(playerId));
        } catch (ExecutionException e) {
            log.error("Couldn't retrieve username from cache");
            pl.setUsername(getUsernameFromPlayerId(playerId));
        }
        pl.createAndSetLog(logType, item.getItemNumber());
        pl.setId(save(pl));
        return pl;
    }

    public GameLog createGameLog(Draw draw, String pbfId, String username, boolean vote) {
        GameLog pl = new GameLog();
        pl.setPbfId(pbfId);
        pl.setUsername(username);
        pl.setPublicLog(username + " has voted " + (vote ? "yes" : "no") + " to undo "
                + draw.getItem().revealPublic() + " with item number " + draw.getItem().getItemNumber());
        pl.setId(save(pl));
        return pl;
    }

    /**
     * Common messages like, user joined game, user withdrew game etc
     */
    public GameLog createCommonPublicLog(String publicMessage, String pbfId, String playerId) {
        GameLog pl = createCommonGameLog(pbfId, playerId);
        pl.setPublicLog(pl.getUsername() + " " + publicMessage);
        pl.setPrivateLog("");
        pl.setId(save(pl));
        return pl;
    }

    public GameLog createCommonPrivateLog(String privateMessage, String pbfId, String playerId) {
        GameLog pl = createCommonGameLog(pbfId, playerId);
        pl.setPrivateLog(pl.getUsername() + " " + privateMessage);
        pl.setPublicLog("");
        pl.setId(save(pl));
        return pl;
    }

    public GameLog createCommonPrivatePublicLog(String message, String pbfId, String playerId) {
        GameLog pl = createCommonGameLog(pbfId, playerId);
        pl.setPrivateLog(pl.getUsername() + " " + message);
        pl.setPublicLog(pl.getUsername() + " " + message);
        pl.setId(save(pl));
        return pl;
    }

    private GameLog createCommonGameLog(String pbfId, String playerId) {
        GameLog pl = new GameLog();
        pl.setPbfId(pbfId);
        try {
            pl.setUsername(CivSingleton.instance().playerCache().get(playerId));
        } catch (Exception e) {
            log.error("Couldn't retrieve username from cache");
            pl.setUsername(getUsernameFromPlayerId(playerId));
        }
        return pl;
    }

    public GameLog findGameLogById(String id) {
        return gameLogCollection.findOneById(id);
    }

    public WriteResult<GameLog, String> updateGameLogById(GameLog gameLog) {
        return gameLogCollection.updateById(gameLog.getId(), gameLog);
    }

    private String getUsernameFromPlayerId(String playerId) {
        return playerCollection.findOneById(playerId).getUsername();
    }

    public List<GameLog> getGameLogs(String pbfId) {
        return gameLogCollection.find(DBQuery.is("pbfId", pbfId)).toArray();
    }

    public List<GameLog> getGameLogsBelongingToPlayer(String pbfId, String username) {
        return gameLogCollection.find(DBQuery.is("pbfId", pbfId).is("username", username)).toArray();
    }

    public void createTradeGameLog(Item item, String pbfId, GameLog.LogType logType, String username) {
        GameLog gl = new GameLog();
        Draw<Item> draw = new Draw<>(pbfId, item.getOwnerId());
        draw.setItem(item);
        gl.setDraw(draw);
        gl.setPbfId(pbfId);
        try {
            gl.setUsername(CivSingleton.instance().playerCache().get(item.getOwnerId()));
        } catch (ExecutionException e) {
            log.error("Couldn't retrieve username from cache");
            gl.setUsername(getUsernameFromPlayerId(item.getOwnerId()));
        }
        gl.createAndSetLog(logType, item.getItemNumber());
        gl.setId(save(gl));

        GameLog gl2 = new GameLog();
        gl2.setPbfId(pbfId);
        gl2.setUsername(username);
        gl2.setPrivateLog(gl.getUsername() + " has received - " + draw.getItem().revealAll());
        gl2.setPublicLog("");
        gl2.setId(save(gl2));
    }

    public GameLog createGameLog(String pbfId, GameLog.LogType logType, String playerId) {
        GameLog pl = new GameLog();
        pl.setPbfId(pbfId);
        try {
            pl.setUsername(CivSingleton.instance().playerCache().get(playerId));
        } catch (ExecutionException e) {
            log.error("Couldn't retrieve username from cache");
            pl.setUsername(getUsernameFromPlayerId(playerId));
        }
        pl.createAndSetLog(logType, 0);
        pl.setId(save(pl));
        return pl;
    }

    public boolean updateGameLog(String pbfId, String oldUsername, String newUsername) {
        List<GameLog> gameLogs = gameLogCollection.find(DBQuery.is("pbfId", pbfId).is("username", oldUsername)).toArray();
        for (GameLog gl : gameLogs) {
            gl.setUsername(newUsername);
            gameLogCollection.updateById(gl.getId(), gl);
        }
        return !gameLogs.isEmpty();
    }

    public void createUndoLog(String pbfId, String message, Item item) {
        Preconditions.checkNotNull(message);
        GameLog log = new GameLog();
        log.setPublicLog("System: " + message + ". Item number #" + item.getItemNumber());
        log.setPrivateLog("System: " + message + ". Item number #" + item.getItemNumber());
        log.setUsername("System");
        log.setPbfId(pbfId);
        save(log);
    }
} @Log4j
public class PlayerAction extends BaseAction {

    private final JacksonDBCollection<Player, String> playerCollection;
    private final JacksonDBCollection<PBF, String> pbfCollection;
    private final JacksonDBCollection<GameLog, String> gameLogCollection;

    private final DrawAction drawAction;

    public PlayerAction(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.gameLogCollection = JacksonDBCollection.wrap(db.getCollection(GameLog.COL_NAME), GameLog.class, String.class);
        this.drawAction = new DrawAction(db);
    }

    /**
     * Returns a set of all the game ids of player
     */
    public Set<String> getGames(Player player) {
        Preconditions.checkNotNull(player);
        log.debug("Getting all games for player " + player.getUsername());
        return player.getGameIds();
    }

    /**
     * Choose a tech for player and store back in the pbf collection
     *
     * @param pbfId    - The pbf id
     * @param techName - The tech
     * @param playerId - The id of player
     */
    public GameLog chooseTech(String pbfId, String techName, String playerId) {
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(techName);

        PBF pbf = pbfCollection.findOneById(pbfId);
        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.error("User with id " + playerId + " has no access to pbf " + pbf.getName());
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }

        Optional<Tech> tech = pbf.getTechs().stream()
                .filter(techToFind -> techToFind.getName().equals(techName))
                .findFirst();
        //if not static then this::cannotFindItem
        Tech chosenTech = tech.orElseThrow(PlayerAction::cannotFindItem);
        chosenTech.setHidden(true);
        chosenTech.setOwnerId(playerId);

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        if (playerhand.getTechsChosen().contains(chosenTech)) {
            log.warn("Player with id " + playerId + " tried to add same tech as they had");
            return null;
        }
        playerhand.getTechsChosen().add(chosenTech);

        pbfCollection.updateById(pbf.getId(), pbf);
        log.debug("Player " + playerId + " chose tech " + chosenTech.getName());

        return super.createLog(chosenTech, pbfId, GameLog.LogType.TECH);
    }

    public boolean removeTech(String pbfId, String techName, String playerId) {
        Preconditions.checkNotNull(techName);
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(playerId);

        PBF pbf = pbfCollection.findOneById(pbfId);
        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.error("User with id " + playerId + " has no access to pbf " + pbf.getName());
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        Tech techToRemove = playerhand.getTechsChosen().stream()
                .filter(tech -> tech.getName().equals(techName))
                .findFirst().orElseThrow(PlayerAction::cannotFindItem);
        boolean removed = playerhand.getTechsChosen().remove(techToRemove);
        if (!removed) {
            log.error("Could not remove tech " + techName + " from player with id " + playerId + " in pbf " + pbf.getName());
            return false;
        }
        pbfCollection.updateById(pbf.getId(), pbf);

        super.createLog(techToRemove, pbfId, GameLog.LogType.REMOVED_TECH);
        return true;
    }

    public boolean endTurn(String pbfId, Player player) {
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(player.getUsername());

        PBF pbf = pbfCollection.findOneById(pbfId);

        if (pbf.getPlayers().get(0).getPlayernumber() > 0) {
            Playerhand playerhand = pbf.getPlayers().stream().filter(Playerhand::isYourTurn).findFirst().get();
            playerhand.setYourTurn(false);

            int nextPlayerNumber = playerhand.getPlayernumber() + 1;
            Playerhand firstPlayer = pbf.getPlayers().stream().filter(p -> p.getPlayernumber() == 1).findFirst().get();
            Playerhand nextPlayer = pbf.getPlayers().stream().filter(p -> p.getPlayernumber() == nextPlayerNumber).findFirst().orElse(firstPlayer);
            nextPlayer.setYourTurn(true);
            SendEmail.sendYourTurn(pbf.getName(), nextPlayer.getEmail(), pbf.getId());

            pbfCollection.updateById(pbf.getId(), pbf);
            return true;

        } else {
            //Old way, this else can be deleted once all games after januar 4 is over

            //Loop through the list and find next starting player
            for (int i = 0; i < pbf.getPlayers().size(); i++) {
                Playerhand playerhand = pbf.getPlayers().get(i);
                if (playerhand.getUsername().equals(player.getUsername())) {
                    playerhand.setYourTurn(false);

                    //Choose next player in line to be starting player
                    Playerhand nextPlayer;
                    if (pbf.getPlayers().size() == (i + 1)) {
                        nextPlayer = pbf.getPlayers().get(0);
                    } else {
                        nextPlayer = pbf.getPlayers().get(i + 1);
                    }
                    nextPlayer.setYourTurn(true);
                    SendEmail.sendYourTurn(pbf.getName(), nextPlayer.getEmail(), pbf.getId());

                    pbfCollection.updateById(pbf.getId(), pbf);
                    return true;
                }
            }

        }
        return false;
    }

    /**
     * Revealing of items are really just saving a public log with the hidden content information
     *
     * @param pbfId
     * @param playerId
     * @param itemDTO  - The item to reveal
     */
    @SuppressWarnings("unchecked")
    public void revealItem(String pbfId, String playerId, ItemDTO itemDTO) {
        Preconditions.checkNotNull(itemDTO);
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(playerId);

        //Check if item can be found on the player
        PBF pbf = pbfCollection.findOneById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);

        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.error("User with id " + playerId + " has no access to pbf " + pbf.getName());
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }

        List<Item> items = playerhand.getItems();

        Optional<SheetName> sheetName = SheetName.find(itemDTO.getSheetName());
        if (!sheetName.isPresent()) {
            log.error("Cannot find Sheetname " + itemDTO.getSheetName());
            throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST)
                    .entity(Entity.json("{\"msg\": \"Cannot find Sheetname " + itemDTO.getSheetName() + "\"}"))
                    .build());
        }

        Optional<Item> itemToRevealOptional = items.stream()
                .filter(it -> it.getItemNumber() == itemDTO.getItemNumber())
                .filter(it -> it.getSheetName() == sheetName.get())
                .filter(Item::isHidden)
                .findFirst();

        if (!itemToRevealOptional.isPresent()) {
            itemToRevealOptional = items.stream()
                    .filter(it -> it.getName().equals(itemDTO.getName()))
                    .filter(it -> it.getSheetName() == sheetName.get())
                    .filter(Item::isHidden)
                    .findFirst();
        }

        if (!itemToRevealOptional.isPresent()) {
            log.warn("Item " + itemDTO.getName() + " already revealed");
            throw new WebApplicationException(Response.status(Response.Status.NOT_MODIFIED)
                    .entity(Entity.json("{\"msg\": \"Item already revealed\"}"))
                    .build());
        }

        boolean isCiv = isCivilization(playerhand, sheetName);

        Item itemToReveal = itemToRevealOptional.get();
        itemToReveal.setHidden(false);
        if (isCiv) {
            Civ civ = setStartingTech(playerId, playerhand, (Civ) itemToReveal);
            pbfCollection.updateById(pbf.getId(), pbf);
            //Create a new log entry
            logAction.createGameLog(itemToReveal, pbf.getId(), GameLog.LogType.REVEAL);
            log.debug("item to be reveal " + itemToReveal);

            //If player has no units, then no need to call this
            if (playerhand.getItems().stream().noneMatch(p -> p instanceof Unit)) {
                drawStartingItems(pbfId, playerId, civ);
            }

            deleteTheOtherCivs(pbfId, playerId, civ);

            if (shouldDrawWonders(pbf)) {
                drawStartingWonders(pbf, playerhand.getPlayerId());
            }

        } else {
            pbfCollection.updateById(pbf.getId(), pbf);
            //Create a new log entry
            logAction.createGameLog(itemToReveal, pbf.getId(), GameLog.LogType.REVEAL);
            log.debug("item to be reveal " + itemToReveal);
        }

    }

    private boolean shouldDrawWonders(PBF pbf) {
        return pbf.getNumOfPlayers() == pbf.getPlayers().size() &&
                pbf.getPlayers().stream().allMatch(p -> p.getCivilization() != null) &&
                pbf.getDiscardedItems().stream()
                        .filter(item -> SheetName.ALL_WONDERS.contains(item.getSheetName()))
                        .count() == 0 &&
                pbf.getPlayers()
                        .stream()
                        .flatMap(p -> p.getItems().stream())
                        .filter(it -> SheetName.ALL_WONDERS.contains(it.getSheetName()))
                        .count() == 0;
    }

    private Civ setStartingTech(String playerId, Playerhand playerhand, Civ civ) {
        playerhand.setCivilization(civ);
        Tech startingTech = civ.getStartingTech();
        startingTech.setHidden(false);
        startingTech.setOwnerId(playerId);
        playerhand.getTechsChosen().add(startingTech);
        return civ;
    }

    private void deleteTheOtherCivs(String pbfId, String playerId, Civ civ) {
        PBF pbf = pbfCollection.findOneById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        Iterator<Item> iterator = playerhand.getItems().iterator();
        boolean deleted = false;
        while (iterator.hasNext()) {
            Item item = iterator.next();
            if (item instanceof Civ && !item.equals(civ)) {
                item.setHidden(true);
                pbf.getDiscardedItems().add(item);
                iterator.remove();
                deleted = true;
                createLog(item, pbf.getId(), GameLog.LogType.DISCARD, playerId);
            }
        }

        if (deleted) {
            pbfCollection.updateById(pbf.getId(), pbf);
        }
    }

    private void drawStartingWonders(PBF pbf, String playerId) {
        createInfoLog(pbf.getId(), "Drawing 4 ancient wonders");
        drawAction.draw(pbf.getId(), playerId, SheetName.ANCIENT_WONDERS);
        drawAction.draw(pbf.getId(), playerId, SheetName.ANCIENT_WONDERS);
        drawAction.draw(pbf.getId(), playerId, SheetName.ANCIENT_WONDERS);
        drawAction.draw(pbf.getId(), playerId, SheetName.ANCIENT_WONDERS);
    }

    private boolean isCivilization(Playerhand playerhand, Optional<SheetName> sheetName) {
        if (sheetName.get() == SheetName.CIV) {
            if (playerhand.getCivilization() != null) {
                log.warn("Cannot choose civilization again");
                throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST)
                        .entity(Entity.json("{\"msg\": \"Civilization already chosen\"}"))
                        .build());
            }
            return true;
        }
        return false;
    }

    /**
     * Only units and wonder for Egypt is drawn
     */
    private void drawStartingItems(String pbfId, String playerId, Civ civ) {
        switch (civ.getName()) {
            case "Germans":
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                break;
            case "Mongols":
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                break;
            case "Zulu":
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                break;
            case "Egyptians":
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                drawAction.draw(pbfId, playerId, SheetName.ANCIENT_WONDERS);
                break;
            default:
                drawAction.draw(pbfId, playerId, SheetName.INFANTRY);
                drawAction.draw(pbfId, playerId, SheetName.ARTILLERY);
                drawAction.draw(pbfId, playerId, SheetName.MOUNTED);
                break;
        }

    }

    /**
     * Revealing of techs are really just saving a public log with the hidden content information
     *
     * @param gameLog
     * @param pbfId
     * @param playerId
     */
    public void revealTech(GameLog gameLog, String pbfId, String playerId) {
        Preconditions.checkNotNull(gameLog);
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(playerId);

        PBF pbf = pbfCollection.findOneById(pbfId);

        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.error("User with id " + playerId + " has no access to pbf " + pbf.getName());
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }

        Draw<?> draw = gameLog.getDraw();
        if (draw == null || draw.getItem() == null) {
            log.error("Couldn't find tech to reveal");
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        Item item = draw.getItem();
        item.setHidden(false);

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        Tech tech = playerhand.getTechsChosen().stream().filter(t -> t.getName().equals(item.getName())).findFirst().orElseThrow(PlayerAction::cannotFindItem);
        tech.setHidden(false);

        gameLogCollection.updateById(gameLog.getId(), gameLog);
        pbfCollection.updateById(pbfId, pbf);

        createLog(item, pbf.getId(), GameLog.LogType.REVEAL, playerId);
    }

    /**
     * Returns the remaining techs the player can choose from
     *
     * @param playerId - The player
     * @param pbfId    - The PBF
     * @return
     */
    public List<Tech> getRemaingTechsForPlayer(String playerId, String pbfId) {
        PBF pbf = pbfCollection.findOneById(pbfId);

        Optional<Playerhand> playerhandOptional = pbf.getPlayers().stream()
                .filter(p -> p.getPlayerId().equals(playerId))
                .findFirst();

        Set<Tech> techsChosen = playerhandOptional.orElseThrow(PlayerAction::cannotFindPlayer)
                .getTechsChosen();

        Playerhand playerhand = playerhandOptional.get();

        if (playerhand.getCivilization() != null && playerhand.getCivilization().getStartingTech() != null) {
            techsChosen.add(playerhandOptional.get().getCivilization().getStartingTech());
        }
        List<Tech> techs = pbf.getTechs();
        techs.removeAll(techsChosen);

        techs.sort(((o1, o2) -> Integer.valueOf(o1.getLevel()).compareTo(o2.getLevel())));
        return techs;
    }

    /**
     * Method that's checks whether it is players turn.
     * Not the same as #checkYourTurn()
     *
     * @param pbfId    - PBF id
     * @param playerId - Player id
     * @return - true if it is players turn
     * @see #checkYourTurn(String, String)
     */
    public boolean isYourTurn(String pbfId, String playerId) {
        PBF pbf = pbfCollection.findOneById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        return playerhand.isYourTurn();
    }

    /**
     * Will send the item to the new owner
     *
     * @param item
     * @param playerId
     * @return
     */
    public boolean tradeToPlayer(ItemDTO item, String playerId) {
        Preconditions.checkNotNull(item);
        Preconditions.checkNotNull(item.getPbfId());
        Preconditions.checkNotNull(item.getOwnerId());

        PBF pbf = pbfCollection.findOneById(item.getPbfId());
        Playerhand fromPlayer = getPlayerhandByPlayerId(playerId, pbf);
        Playerhand toPlayer = getPlayerhandByPlayerId(item.getOwnerId(), pbf);
        Optional<SheetName> dtoSheet = SheetName.find(item.getSheetName());
        if (!dtoSheet.isPresent()) {
            log.error("Couldn't find sheetname " + item.getSheetName());
            throw cannotFindItem();
        }

        Optional<Item> tradableItem = fromPlayer.getItems().stream()
                .filter(it -> it instanceof Tradable)
                .filter(it -> it.getItemNumber() == item.getItemNumber())
                .filter(it -> it.getName().equalsIgnoreCase(item.getName()))
                .findFirst();

        if (!tradableItem.isPresent()) {
            tradableItem = fromPlayer.getItems().stream()
                    .filter(it -> it instanceof Tradable)
                    .filter(it -> it.getSheetName() == dtoSheet.get())
                    .filter(it -> it.getName().equalsIgnoreCase(item.getName()))
                    .findFirst();
        }

        if (!tradableItem.isPresent()) {
            throw cannotFindItem();
        }

        Item itemToTrade = tradableItem.get();

        boolean remove = fromPlayer.getItems().remove(itemToTrade);
        if (!remove) {
            log.error("Didn't find item from playerhand: " + item);
            return false;
        }
        toPlayer.getItems().add(itemToTrade);

        itemToTrade.setOwnerId(toPlayer.getPlayerId());
        pbfCollection.updateById(pbf.getId(), pbf);
        logAction.createTradeGameLog(itemToTrade, pbf.getId(), GameLog.LogType.TRADE_BETWEEN_PLAYERS, fromPlayer.getUsername());
        return true;
    }

    public void discardItem(String pbfId, String playerId, ItemDTO itemdto) {
        PBF pbf = pbfCollection.findOneById(pbfId);

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);

        //Find the item, then delete it
        Optional<Item> itemToDeleteOptional = playerhand.getItems().stream()
                .filter(item -> item.getItemNumber() == itemdto.getItemNumber())
                .filter(item -> item.getName().equalsIgnoreCase(itemdto.getName()))
                .findFirst();

        if (!itemToDeleteOptional.isPresent()) {
            Optional<SheetName> dtoSheet = SheetName.find(itemdto.getSheetName());
            if (!dtoSheet.isPresent()) {
                log.error("Couldn't find sheetname " + itemdto.getSheetName());
                throw cannotFindItem();
            }
            //Find the item, then delete it
            itemToDeleteOptional = playerhand.getItems().stream()
                    .filter(item -> item.getSheetName() == dtoSheet.get())
                    .filter(item -> item.getName().equalsIgnoreCase(itemdto.getName()))
                    .findFirst();
        }
        if (!itemToDeleteOptional.isPresent()) throw cannotFindItem();

        Item itemToDelete = itemToDeleteOptional.get();
        itemToDelete.setHidden(true);
        //itemToDelete.setOwnerId(null); //I think I need this in case of undo

        if (playerhand.getItems().remove(itemToDeleteOptional.get())) {
            pbf.getDiscardedItems().add(itemToDeleteOptional.get());
            createLog(itemToDelete, pbf.getId(), GameLog.LogType.DISCARD, playerId);
            pbfCollection.updateById(pbf.getId(), pbf);
            return;
        }
        log.error("Found the item " + itemToDelete + " , but couldn't delete it for some reason");
        throw cannotFindItem();
    }

    public Player getPlayerById(String playerId) {
        return playerCollection.findOneById(playerId);
    }

    public Set<Tech> getPlayersTechs(String pbfId, String playerId) {
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(playerId);

        PBF pbf = pbfCollection.findOneById(pbfId);
        return pbf.getPlayers().stream().filter(p -> p.getPlayerId().equals(playerId))
                .findFirst().orElseThrow(PlayerAction::cannotFindPlayer)
                .getTechsChosen();

    }

    /**
     * Returns the id to the player created
     *
     * @return the id of the newly created player
     * @throws PlayerExistException - Throws this exception if username already exists
     */
    @SneakyThrows
    public String createPlayer(String usernameEncoded, String passwordEncoded, String emailEncoded) throws PlayerExistException {
        Preconditions.checkNotNull(usernameEncoded);
        Preconditions.checkNotNull(passwordEncoded);
        Preconditions.checkNotNull(emailEncoded);

        String username = URLDecoder.decode(usernameEncoded, "UTF-8");
        String email = URLDecoder.decode(emailEncoded, "UTF-8");
        String password = URLDecoder.decode(passwordEncoded, "UTF-8");

        if (CivSingleton.instance().playerCache().asMap().containsValue(username)) {
            throw new PlayerExistException();
        }

        Player player = new Player();
        player.setUsername(username);
        String decodedPassword = new String(Base64.getDecoder().decode(password), "UTF-8");

        player.setPassword(DigestUtils.sha1Hex(decodedPassword));
        player.setEmail(email);
        WriteResult<Player, String> insert = playerCollection.insert(player);
        log.info(String.format("Saving player with id %s", insert.getSavedId()));
        return insert.getSavedId();
    }

    public void newPassword(String username, String newPass) throws Exception {
        Player player = playerCollection.findOne(DBQuery.is(Player.USERNAME, username));
        if (player == null) {
            throw new WebApplicationException(Response.Status.BAD_REQUEST);
        }

        String password = URLDecoder.decode(newPass, "UTF-8");
        player.setPassword(DigestUtils.sha1Hex(password));
        playerCollection.updateById(player.getId(), player);
    }

    public boolean newPassword(ForgotpassDTO forgotpassDTO) {
        Preconditions.checkNotNull(forgotpassDTO.getEmail());
        Preconditions.checkNotNull(forgotpassDTO.getNewpassword());

        Player player = playerCollection.findOne(DBQuery.is(Player.EMAIL, forgotpassDTO.getEmail()));
        if (player == null) {
            log.error("Couldn't find user by email " + forgotpassDTO.getEmail());
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        player.setNewPassword(forgotpassDTO.getNewpassword());
        playerCollection.updateById(player.getId(), player);
        return SendEmail.sendMessage(player.getEmail(),
                "Please verify your email",
                "Your password was requested to be changed. If you want to change your password then please press this link: "
                        + SendEmail.REST_URL + "api/auth/verify/" + player.getId(), player.getId());
    }

    public boolean verifyPassword(String playerId) {
        Player player = playerCollection.findOneById(playerId);
        if (player != null && !Strings.isNullOrEmpty(player.getNewPassword())) {
            try {
                String password = URLDecoder.decode(player.getNewPassword(), "UTF-8");
                player.setPassword(DigestUtils.sha1Hex(password));
                player.setNewPassword(null);
                playerCollection.updateById(player.getId(), player);
                return true;
            } catch (UnsupportedEncodingException e) {
                log.error("Couldn't write password ", e);
                throw new WebApplicationException(Response.Status.BAD_REQUEST);
            }
        }

        return false;
    }

    public GameLog chooseSocialPolicy(String pbfId, String socialPolicyName, String playerId) {
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(socialPolicyName);

        PBF pbf = pbfCollection.findOneById(pbfId);
        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.error("User with id " + playerId + " has no access to pbf " + pbf.getName());
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }

        Optional<SocialPolicy> socialPolicyOptional = pbf.getSocialPolicies().stream()
                .filter(it -> it.getName().equals(socialPolicyName))
                .findFirst();
        //if not static then this::cannotFindItem
        SocialPolicy socialPolicy = socialPolicyOptional.orElseThrow(PlayerAction::cannotFindItem);

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        if (playerhand.getSocialPolicies().contains(socialPolicy)) {
            log.warn("Player with id " + playerId + " tried to add same social policy as they had");
            return null;
        }

        if (playerhand.getSocialPolicies().stream().anyMatch(sp -> sp.getName().equals(socialPolicy.getFlipside()))) {
            log.warn("Player with id " + playerId + " tried to add a social policy on same flipside");
            throw new WebApplicationException(Response.Status.BAD_REQUEST);
        }

        SocialPolicy sp = new SocialPolicy(socialPolicyName);
        sp.setFlipside(socialPolicy.getFlipside());
        sp.setOwnerId(playerId);
        sp.setHidden(true);

        playerhand.getSocialPolicies().add(sp);

        pbfCollection.updateById(pbf.getId(), pbf);
        log.debug("Player " + playerId + " chose social policy " + sp.getName());

        return super.createLog(sp, pbfId, GameLog.LogType.SOCIAL_POLICY, playerId);
    }

    public List<AllTechsDTO> getTechsForAllPlayers(String pbfId) {
        Preconditions.checkNotNull(pbfId);

        PBF pbf = pbfCollection.findOneById(pbfId);
        return pbf.getPlayers().stream()
                .filter(p -> p.getCivilization() != null)
                .map(p -> new AllTechsDTO(p.getCivilization().getName(), p.getColor(),
                        p.getTechsChosen().stream().filter(t -> !t.isHidden())
                                .map(t -> new TechDTO(t.getName(), t.getLevel()))
                                .collect(Collectors.toList())))
                .collect(Collectors.toList());

    }

    public void saveNote(String pbfId, String playerId, MessageDTO messageDTO) {
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(playerId);

        PBF pbf = pbfCollection.findOneById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        playerhand.setGamenote(messageDTO.getMessage());
        pbfCollection.updateById(pbfId, pbf);
    }

    public void takeTurnButton(String pbfId, String playerId) {
        Preconditions.checkNotNull(pbfId);
        Preconditions.checkNotNull(playerId);
        PBF pbf = pbfCollection.findOneById(pbfId);

        pbf.getPlayers().stream()
                .filter(p -> p.isYourTurn())
                .forEach(p -> p.setYourTurn(false));

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        playerhand.setYourTurn(true);

        pbfCollection.updateById(pbfId, pbf);

        super.createCommonPublicLog("took turn button", pbfId, playerId);
    }
} @Log4j
public class TurnAction extends BaseAction {

    private final JacksonDBCollection<PBF, String> pbfCollection;

    public TurnAction(DB db) {
        super(db);
        this.pbfCollection = JacksonDBCollection.wrap(db.getCollection(PBF.COL_NAME), PBF.class, String.class);
    }

    public void updateSOT(String pbfId, String playerId, TurnDTO turnDTO) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        updateTurn(playerId, turnDTO, pbf, playerhand);

        Thread thread = new Thread(() -> {
            pbf.getPlayers()
                    .stream()
                    .filter(p -> !p.getUsername().equals(playerhand.getUsername()))
                    .filter(CivUtil::shouldSendEmailInGame)
                    .forEach(p -> SendEmail.sendMessage(p.getEmail(), "Start of turn updated", playerhand.getUsername() + " has updated start of turn with " +
                                    "the following order\n:" + turnDTO.getOrder()
                                    + ".\n\nLogin to " + SendEmail.gamelink(pbfId) + " to see the order", playerhand.getPlayerId())
                    );
        });
        thread.start();

        pbfCollection.updateById(pbfId, pbf);
        super.createLog(pbfId, GameLog.LogType.SOT, playerId);
    }

    public void updateTrade(String pbfId, String playerId, TurnDTO turnDTO) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        updateTurn(playerId, turnDTO, pbf, playerhand);

        Thread thread = new Thread(() -> {
            pbf.getPlayers()
                    .stream()
                    .filter(p -> !p.getUsername().equals(playerhand.getUsername()))
                    .filter(CivUtil::shouldSendEmailInGame)
                    .forEach(p -> SendEmail.sendMessage(p.getEmail(), "Trade updated", playerhand.getUsername() + " has updated trade with " +
                                    "the following order:\n" + turnDTO.getOrder()
                                    + ".\n\nLogin to " + SendEmail.gamelink(pbfId) + " to see the order", playerhand.getPlayerId())
                    );
        });
        thread.start();

        pbfCollection.updateById(pbfId, pbf);
        super.createLog(pbfId, GameLog.LogType.TRADE, playerId);
    }

    public void updateCM(String pbfId, String playerId, TurnDTO turnDTO) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        updateTurn(playerId, turnDTO, pbf, playerhand);

        Thread thread = new Thread(() -> {
            pbf.getPlayers()
                    .stream()
                    .filter(p -> !p.getUsername().equals(playerhand.getUsername()))
                    .filter(CivUtil::shouldSendEmailInGame)
                    .forEach(p -> {
                                SendEmail.sendMessage(p.getEmail(), "City management updated", playerhand.getUsername() + " has updated city management with " +
                                        "the following order:\n" + turnDTO.getOrder()
                                        + ".\n\nLogin to " + SendEmail.gamelink(pbfId) + " to see the order", playerhand.getPlayerId());
                            }
                    );
        });
        thread.start();

        pbfCollection.updateById(pbfId, pbf);
        super.createLog(pbfId, GameLog.LogType.CM, playerId);
    }

    public void updateMovement(String pbfId, String playerId, TurnDTO turnDTO) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        updateTurn(playerId, turnDTO, pbf, playerhand);

        Thread thread = new Thread(() -> {
            pbf.getPlayers()
                    .stream()
                    .filter(p -> !p.getUsername().equals(playerhand.getUsername()))
                    .filter(CivUtil::shouldSendEmailInGame)
                    .forEach(
                            p -> SendEmail.sendMessage(p.getEmail(), "Movement updated", playerhand.getUsername() + " has updated movement with " +
                                    "the following order:\n" + turnDTO.getOrder()
                                    + ".\n\nLogin to " + SendEmail.gamelink(pbfId) + " to see the order", playerhand.getPlayerId())
                    );
        });
        thread.start();

        pbfCollection.updateById(pbfId, pbf);
        super.createLog(pbfId, GameLog.LogType.MOVEMENT, playerId);
    }

    public void updateResearch(String pbfId, String playerId, TurnDTO turnDTO) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        updateTurn(playerId, turnDTO, pbf, playerhand);

        Thread thread = new Thread(() -> {
            pbf.getPlayers()
                    .stream()
                    .filter(p -> !p.getUsername().equals(playerhand.getUsername()))
                    .filter(CivUtil::shouldSendEmailInGame)
                    .forEach(p -> SendEmail.sendMessage(p.getEmail(), "Research updated", playerhand.getUsername() + " has updated research with " +
                                    "the following order:\n" + turnDTO.getOrder()
                                    + ".\n\nLogin to " + SendEmail.gamelink(pbfId) + " to see the order", playerhand.getPlayerId())
                    );
        });
        thread.start();

        pbfCollection.updateById(pbfId, pbf);
        super.createLog(pbfId, GameLog.LogType.RESEARCH, playerId);
    }

    /**
     * Each player can add a new turn so they can start new to write new orders
     */
    public void addNewTurn(String pbfId, String playerId, int turnNumber) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);

        Optional<PlayerTurn> turnOptional = playerhand.getPlayerTurns().stream()
                .filter(p -> p.getTurnNumber() == turnNumber)
                .findAny();

        if (!turnOptional.isPresent()) {
            playerhand.getPlayerTurns().add(new PlayerTurn(playerhand.getUsername(), turnNumber));
        }
    }

    private PlayerTurn getPlayerTurn(TurnDTO turnDTO, Set<PlayerTurn> playerturns, String username) {
        PlayerTurn playerTurn = playerturns.stream()
                .filter(t -> t.getTurnNumber() == turnDTO.getTurnNumber())
                .findFirst()
                .orElse(new PlayerTurn(username, turnDTO.getTurnNumber()));
        playerTurn.setUsername(username);
        return playerTurn;
    }

    public List<PlayerTurn> getAllPublicTurns(String pbfId) {
        PBF pbf = findPBFById(pbfId);

        return pbf.getPublicTurns().values().stream()
                .sorted()
                .map(p -> {
                    //Remove the last order in the history
                    p.getSotHistory().remove(p.getSot());
                    p.getTradeHistory().remove(p.getTrade());
                    p.getCmHistory().remove(p.getCm());
                    p.getMovementHistory().remove(p.getMovement());
                    p.getResearchHistory().remove(p.getResearch());
                    return p;
                })
                .collect(toList());

    }

    public Set<PlayerTurn> getPlayersTurns(String pbfId, String playerId) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        Set<PlayerTurn> playerTurns = playerhand.getPlayerTurns();
        if (playerTurns.isEmpty()) {
            playerhand.getPlayerTurns().add(new PlayerTurn(playerhand.getUsername(), 1));
            pbfCollection.updateById(pbfId, pbf);
        }

        return playerTurns;
    }

    private void updateTurn(String playerId, TurnDTO turnDTO, PBF pbf, Playerhand playerhand) {
        if (!SecurityCheck.hasUserAccess(pbf, playerId)) {
            log.error("User with id " + playerId + " has no access to pbf " + pbf.getName());
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        Set<PlayerTurn> playerturns = playerhand.getPlayerTurns();
        PlayerTurn playerTurn = updatePrivatePlayerturn(turnDTO, playerhand, playerturns);

        addTurnInPBF(pbf, playerTurn);
    }

    private void addTurnInPBF(PBF pbf, PlayerTurn playerTurn) {
        // Cant get map serialization to work in jackson
        //final TurnKey turnKey = new TurnKey(playerTurn.getTurnNumber(), playerTurn.getUsername());
        pbf.getPublicTurns().put(playerTurn.getTurnNumber() + playerTurn.getUsername(), playerTurn);
        pbfCollection.updateById(pbf.getId(), pbf);
    }

    private PlayerTurn updatePrivatePlayerturn(TurnDTO turnDTO, Playerhand playerhand, Set<PlayerTurn> playerturns) {
        PlayerTurn pt = getPlayerTurn(turnDTO, playerturns, playerhand.getUsername());

        if ("SOT".equalsIgnoreCase(turnDTO.getPhase())) {
            pt.setSot(turnDTO.getOrder());
            pt.getSotHistory().add(turnDTO.getOrder());
        } else if ("Trade".equalsIgnoreCase(turnDTO.getPhase())) {
            pt.setTrade(turnDTO.getOrder());
            pt.getTradeHistory().add(turnDTO.getOrder());
        } else if ("CM".equalsIgnoreCase(turnDTO.getPhase())) {
            pt.setCm(turnDTO.getOrder());
            pt.getCmHistory().add(turnDTO.getOrder());
        } else if ("Movement".equalsIgnoreCase(turnDTO.getPhase())) {
            pt.setMovement(turnDTO.getOrder());
            pt.getMovementHistory().add(turnDTO.getOrder());
        } else if ("Research".equalsIgnoreCase(turnDTO.getPhase())) {
            pt.setResearch(turnDTO.getOrder());
            pt.getResearchHistory().add(turnDTO.getOrder());
        }
        playerhand.getPlayerTurns().add(pt);
        return pt;
    }

    public void lockOrUnlockTurn(String pbfId, String playerId, TurnDTO turnDTO) {
        PBF pbf = findPBFById(pbfId);
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        PlayerTurn playerTurn = playerhand.getPlayerTurns().stream()
                .filter(p -> p.getTurnNumber() == turnDTO.getTurnNumber())
                .findFirst()
                .orElseThrow(PlayerAction::cannotFindItem);

        playerTurn.setDisabled(turnDTO.isLocked());
        String message = turnDTO.isLocked() ? " has locked in turn " + turnDTO.getTurnNumber() : " has re-opened turn " + turnDTO.getTurnNumber();
        createCommonPublicLog(message, pbfId, playerId);
        pbfCollection.updateById(pbfId, pbf);
    }
} @Log4j
public class UndoAction extends BaseAction {
    private final JacksonDBCollection<PBF, String> pbfCollection;
    private final JacksonDBCollection<GameLog, String> gameLogCollection;

    public UndoAction(DB db) {
        super(db);
        this.pbfCollection = JacksonDBCollection.wrap(db.getCollection(PBF.COL_NAME), PBF.class, String.class);
        this.gameLogCollection = JacksonDBCollection.wrap(db.getCollection(GameLog.COL_NAME), GameLog.class, String.class);
    }

    private boolean putDrawnItemBackInPBF(PBF pbf, String playerId, Item item) {
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        if (item instanceof Tech) {
            //Remove from tech
            if (playerhand.getTechsChosen().remove(item)) {
                logAction.createUndoLog(pbf.getId(), "has removed " + item.getName() + " from " + playerhand.getUsername(), item);
                log.debug("Successfully undoed tech");
            } else if (pbf.getDiscardedItems().remove(item)) {
                playerhand.getTechsChosen().add((Tech) item);
                logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
            } else {
                log.error("Didn't find tech to remove from playerhand: " + item);
                return false;
            }
        } else {
            if (playerhand.getItems().remove(item)) {
                item.setHidden(true);
                logAction.createUndoLog(pbf.getId(), "has removed " + item.getName() + " from " + playerhand.getUsername() + " and put back in the deck. Deck is reshuffled", item);
                pbf.getItems().add(item);
                shufflePBFTwice(pbf);
                log.debug("Successfully undoed item");
            } else if (pbf.getDiscardedItems().remove(item)) {
                item.setHidden(true);
                playerhand.getItems().add(item);
                logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
            } else if (pbf.getItems().remove(item)) {
                //In rare cases the item is put back to the player
                item.setHidden(true);
                playerhand.getItems().add(item);
                logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
            } else {
                log.error("Didn't find item to remove from playerhand: " + item);
                return false;
            }
        }

        pbfCollection.updateById(pbf.getId(), pbf);
        return true;
    }

    private boolean putDrawnItemBackInPBF(PBF pbf, Draw draw) {
        Preconditions.checkNotNull(pbf);
        Preconditions.checkNotNull(draw);
        Preconditions.checkNotNull(draw.getItem());

        Item item = draw.getItem();
        return putDrawnItemBackInPBF(pbf, item.getOwnerId(), draw);
    }

    private boolean putDrawnItemBackInPBF(PBF pbf, String playerId, Draw draw) {
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        Item item = draw.getItem();
        if (item instanceof Tech) {
            //Remove from tech
            if (playerhand.getTechsChosen().remove(item)) {
                logAction.createUndoLog(pbf.getId(), "has removed " + item.getName() + " from " + playerhand.getUsername(), item);
                log.debug("Successfully undoed tech");
            } else if (pbf.getDiscardedItems().remove(item)) {
                playerhand.getTechsChosen().add((Tech) item);
                logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
            } else {
                log.error("Didn't find tech to remove from playerhand: " + item);
                return false;
            }
        } else {
            if (!Strings.isNullOrEmpty(draw.getGameLogId())) {
                GameLog gamelog = gameLogCollection.findOneById(draw.getGameLogId());
                if (gamelog.getPrivateLog().contains("discarded")) {
                    if (pbf.getDiscardedItems().remove(item)) {
                        item.setHidden(true);
                        playerhand.getItems().add(item);
                        logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
                    }
                } else if (gamelog.getPrivateLog().contains("drew") && !gamelog.getPrivateLog().contains("barbarian")) {
                    if (playerhand.getItems().remove(item) || pbf.getDiscardedItems().remove(item)) {
                        item.setHidden(true);
                        logAction.createUndoLog(pbf.getId(), "has removed " + item.getName() + " from " + playerhand.getUsername() + " and put back in the deck. Deck is reshuffled", item);
                        pbf.getItems().add(item);
                        shufflePBFTwice(pbf);
                    }
                } else if (gamelog.getPrivateLog().contains("drew") && gamelog.getPrivateLog().contains("barbarian")) {
                    pbf.getItems().addAll(playerhand.getBarbarians());
                    playerhand.getBarbarians().clear();
                    logAction.createUndoLog(pbf.getId(), "has removed barbarians from " + playerhand.getUsername() + " and put back in the deck. Deck is reshuffled", item);
                    shufflePBFTwice(pbf);
                } else if (pbf.getItems().remove(item)) {
                    //In rare cases the item is put back to the player (Not sure if I need this)
                    item.setHidden(true);
                    playerhand.getItems().add(item);
                    logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
                } else {
                    log.error("Didn't find item to remove from playerhand: " + item);
                    return false;
                }
            } else {
                //Backward compability
                if (playerhand.getItems().remove(item)) {
                    item.setHidden(true);
                    logAction.createUndoLog(pbf.getId(), "has removed " + item.getName() + " from " + playerhand.getUsername() + " and put back in the deck. Deck is reshuffled", item);
                    pbf.getItems().add(item);
                    shufflePBFTwice(pbf);
                    log.debug("Successfully undoed item");
                } else if (pbf.getDiscardedItems().remove(item)) {
                    item.setHidden(true);
                    playerhand.getItems().add(item);
                    logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
                } else if (pbf.getItems().remove(item)) {
                    //In rare cases the item is put back to the player
                    item.setHidden(true);
                    playerhand.getItems().add(item);
                    logAction.createUndoLog(pbf.getId(), "has added back " + item.getName() + " to " + playerhand.getUsername(), item);
                } else {
                    log.error("Didn't find item to remove from playerhand: " + item);
                    return false;
                }
            }
        }

        pbfCollection.updateById(pbf.getId(), pbf);
        return true;
    }

    private void shufflePBFTwice(PBF pbf) {
        Collections.shuffle(pbf.getItems(), new Random(System.nanoTime()));
        Collections.shuffle(pbf.getItems(), new Random(System.nanoTime()));
    }

    /**
     * Will perform vote, and if all votes are successfull, item is put back in the deck
     * <p>
     * A vote must have already been initiated
     *
     * @param gameLog
     * @param playerId
     * @param vote
     * @return
     */
    public GameLog vote(GameLog gameLog, String playerId, boolean vote) {
        Preconditions.checkNotNull(gameLog);
        Preconditions.checkNotNull(gameLog.getDraw());
        Preconditions.checkNotNull(gameLog.getDraw().getUndo());

        PBF pbf = pbfCollection.findOneById(gameLog.getPbfId());
        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);

        if (gameLog.getDraw() == null || gameLog.getDraw().getUndo() == null) {
            log.error("This item cannot be undone. Nothing to undo.");
            throw new WebApplicationException(Response.status(Response.Status.PRECONDITION_FAILED)
                    .build());
        }

        gameLog.getDraw().getUndo().vote(playerId, vote);
        gameLog.getDraw().setGameLogId(gameLog.getId());
        createLog(gameLog.getDraw(), pbf.getId(), playerhand.getUsername(), vote);

        Optional<Boolean> resultOfVotes = gameLog.getDraw().getUndo().getResultOfVotes();
        if (resultOfVotes.isPresent() && resultOfVotes.get()) {
            log.info("Everyone has performed a vote, so we put item back in the deck");
            gameLog.getDraw().getUndo().setDone(true);
            putDrawnItemBackInPBF(pbf, gameLog.getDraw());
        }
        gameLogCollection.updateById(gameLog.getId(), gameLog);
        return gameLog;
    }

    /**
     * Will request for undo and initiate a vote
     *
     * @param logContainingItemToUndo
     * @param playerId
     * @return
     */
    public void initiateUndo(GameLog logContainingItemToUndo, String playerId) {
        Preconditions.checkNotNull(logContainingItemToUndo);
        Preconditions.checkNotNull(logContainingItemToUndo.getDraw());

        Draw<?> draw = logContainingItemToUndo.getDraw();
        if (draw.getUndo() != null) {
            log.error("Cannot initiate a undo. Its already been initiated");
            throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST)
                    .build());
        }
        PBF pbf = pbfCollection.findOneById(logContainingItemToUndo.getPbfId());
        if (!pbf.getPlayers().stream().anyMatch(p -> p.getPlayerId().equals(playerId))) {
            log.error("Couldn't find playerId " + playerId + " in PBF's players");
            throw PlayerAction.cannotFindPlayer();
        }

        draw.setUndo(new Undo(pbf.getNumOfPlayers(), playerId));
        gameLogCollection.updateById(logContainingItemToUndo.getId(), logContainingItemToUndo);

        createLog(draw.getItem(), pbf.getId(), GameLog.LogType.UNDO, playerId);
    }

    public List<GameLog> getAllActiveUndos(String pbfId) {
        List<GameLog> gameLogs = gameLogCollection.find(DBQuery.is("pbfId", pbfId), new BasicDBObject()).toArray();

        return gameLogs.stream()
                .filter(GameLog::hasActiveUndo)
                .collect(Collectors.toList());
    }

    public List<GameLog> getPlayersActiveUndoes(String pbfId, String username) {
        List<GameLog> gamelogs = gameLogCollection.find(DBQuery.is("pbfId", pbfId).is("username", username), new BasicDBObject()).toArray();

        return gamelogs.stream()
                .filter(GameLog::hasActiveUndo)
                .collect(Collectors.toList());
    }

    public List<GameLog> getAllFinishedUndos(String pbfId) {
        List<GameLog> gameLogs = gameLogCollection.find(DBQuery.is("pbfId", pbfId), new BasicDBObject()).toArray();

        return gameLogs.stream()
                .filter(log -> log.getDraw() != null && log.getDraw().getUndo() != null && log.getDraw().getUndo().isDone())
                .collect(Collectors.toList());
    }

    public void playerPutsItemBackInDeck(String pbfId, String playerId, ItemDTO itemdto) {
        PBF pbf = pbfCollection.findOneById(pbfId);

        Playerhand playerhand = getPlayerhandByPlayerId(playerId, pbf);
        Optional<SheetName> dtoSheet = SheetName.find(itemdto.getSheetName());
        if (!dtoSheet.isPresent()) {
            log.error("Couldn't find sheetname " + itemdto.getSheetName());
            throw cannotFindItem();
        }

        //Find the item, then putback to deck
        Optional<Item> itemToPutBack = playerhand.getItems().stream()
                .filter(item -> item.getSheetName() == dtoSheet.get() && item.getName().equals(itemdto.getName()))
                .findAny();

        if (!itemToPutBack.isPresent()) {
            throw cannotFindItem();
        }

        Item item = itemToPutBack.get();
        boolean ok = putDrawnItemBackInPBF(pbf, playerId, item);
        if (!ok) {
            throw cannotFindItem();
        }
    }
} @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;
} @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();
    }
} @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;
    }
} @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);
    }
} @Getter
@Setter
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
public class PlayerTurn implements Comparable<PlayerTurn> {
    private int turnNumber = 1;
    private String username = "";
    private boolean disabled;
    private Set<String> sotHistory = new HashSet<>();
    private Set<String> tradeHistory = new HashSet<>();
    private Set<String> cmHistory = new HashSet<>();
    private Set<String> movementHistory = new HashSet<>();
    private Set<String> researchHistory = new HashSet<>();

    private String sot = "";
    private String trade = "";
    private String cm = "";
    private String movement = "";
    private String research = "";

    public PlayerTurn(String username, int turnNumber) {
        this.username = username;
        this.turnNumber = turnNumber;
    }

    /**
     * Only one instance of username and turnNumber
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PlayerTurn that = (PlayerTurn) o;
        return Objects.equals(turnNumber, that.turnNumber) &&
                Objects.equals(username, that.username);
    }

    /**
     * Only one instance of username and turnNumber
     */
    @Override
    public int hashCode() {
        return Objects.hash(turnNumber, username);
    }

    /**
     * If turn locked, then mark as end and increase turnNumber;
     */
    @JsonIgnore
    public boolean endTurn() {
        turnNumber++;
        return true;
    }

    @Override
    public int compareTo(PlayerTurn o) {
        int v = Integer.valueOf(turnNumber).compareTo(o.getTurnNumber());
        if (v != 0) {
            return v;
        }
        return username.compareTo(o.getUsername());
    }
} @JsonRootName("tech")
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@NoArgsConstructor
@EqualsAndHashCode(of = {"name"})
public class Tech implements Item, Level, Image {
    @JsonIgnore
    public static final int LEVEL_1 = 1;
    @JsonIgnore
    public static final int LEVEL_2 = 2;
    @JsonIgnore
    public static final int LEVEL_3 = 3;
    @JsonIgnore
    public static final int LEVEL_4 = 4;
    @JsonIgnore
    public static final int LEVEL_5 = 5;

    @JsonIgnore
    public static final Tech SPACE_FLIGHT = new Tech("Space Flight", LEVEL_5, 0);

    @NotEmpty
    private String name;
    private String type;
    private String description;
    private boolean used;
    private boolean hidden = true;
    private String ownerId;
    private int level;
    private String image;
    private SheetName sheetName;
    private int itemNumber;

    public Tech(String name, int level) {
        this.name = name;
        this.level = level;
        hidden = true;
    }

    public Tech(String name, int level, int itemNumber) {
        this.name = name;
        this.level = level;
        hidden = true;
        this.itemNumber = itemNumber;
    }

    @Override
    public SheetName getSheetName() {
        switch (this.level) {
            case LEVEL_1:
                return sheetName = SheetName.LEVEL_1_TECH;
            case LEVEL_2:
                return sheetName = SheetName.LEVEL_2_TECH;
            case LEVEL_3:
                return sheetName = SheetName.LEVEL_3_TECH;
            case LEVEL_4:
                return sheetName = SheetName.LEVEL_4_TECH;
            case LEVEL_5:
                return sheetName = SheetName.LEVEL_5_TECH;
        }

        throw new IllegalArgumentException("Unknown tech level " + level);
    }

    @Override
    public String revealPublic() {
        return getSheetName().getName();
    }

    @Override
    public String revealAll() {
        return name;
    }

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

    @JsonGetter("image")
    @Override
    public String getImage() {
        image = name + ".png";
        return image.replaceAll(" ", "");
    }
}
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.mongodb.DB;
import io.dropwizard.auth.Auth;
import lombok.extern.log4j.Log4j;
import no.asgari.civilization.server.action.GameAction;
import no.asgari.civilization.server.action.GameLogAction;
import no.asgari.civilization.server.action.PlayerAction;
import no.asgari.civilization.server.action.TurnAction;
import no.asgari.civilization.server.action.UndoAction;
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.model.Chat;
import no.asgari.civilization.server.model.GameLog;
import no.asgari.civilization.server.model.PBF;
import no.asgari.civilization.server.model.Player;
import no.asgari.civilization.server.model.PlayerTurn;
import no.asgari.civilization.server.model.Tech;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
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.resource;




@Path("game")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Log4j
public class GameResource {
    private final DB db;
    private final GameAction gameAction;
    private final GameLogAction gameLogAction;