public enum SheetName {
    CIV("Civ"), CULTURE_1("Culture I"), CULTURE_2("Culture II"),
    CULTURE_3("Culture III"), GREAT_PERSON("Great Person"), INFANTRY("Infantry"), ARTILLERY("Artillery"), MOUNTED("Mounted"),
    AIRCRAFT("Aircraft"), VILLAGES("Villages"), HUTS("Huts"), WONDERS("Wonders"), ANCIENT_WONDERS("Ancient Wonders"), MEDIEVAL_WONDERS("Medieval Wonders"),
    MODERN_WONDERS("Modern Wonders"), TILES("Tiles"), CITY_STATES("City-states"), LEVEL_1_TECH("Level 1 Tech"), LEVEL_2_TECH("Level 2 Tech"),
    LEVEL_3_TECH("Level 3 Tech"), LEVEL_4_TECH("Level 4 Tech"), LEVEL_5_TECH("Level 5 Tech"), SOCIAL_POLICY("Social Policy");

    public static final EnumSet<SheetName> SHEETS =
            EnumSet.of(CIV, CULTURE_1, CULTURE_2, CULTURE_3,
                    GREAT_PERSON, INFANTRY, ARTILLERY, MOUNTED, AIRCRAFT,
                    VILLAGES, HUTS, WONDERS, ANCIENT_WONDERS, MEDIEVAL_WONDERS, MODERN_WONDERS, TILES, CITY_STATES,
                    LEVEL_1_TECH, LEVEL_2_TECH, LEVEL_3_TECH, LEVEL_4_TECH, LEVEL_5_TECH, SOCIAL_POLICY);
    public static final EnumSet<SheetName> TECHS = EnumSet.of(LEVEL_1_TECH, LEVEL_2_TECH, LEVEL_3_TECH, LEVEL_4_TECH, LEVEL_5_TECH);
    public static final EnumSet<SheetName> UNITS = EnumSet.of(AIRCRAFT, ARTILLERY, INFANTRY, MOUNTED);
    public static final EnumSet<SheetName> CULTURE_CARD = EnumSet.of(CULTURE_1, CULTURE_2, CULTURE_3);
    public static final EnumSet<SheetName> ALL_WONDERS = EnumSet.of(ANCIENT_WONDERS, MEDIEVAL_WONDERS, MODERN_WONDERS);
    public static final EnumSet<SheetName> SHUFFLABLE_ITEMS = EnumSet.of(AIRCRAFT, ARTILLERY, INFANTRY, MOUNTED,
            GREAT_PERSON, CULTURE_1, CULTURE_2, CULTURE_3, CIV);
    private String label;

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

    public static Optional<SheetName> find(String name) {
        String spacesRemovedName = name.replaceAll("\\s", "");
        Optional<SheetName> found = SHEETS.stream()
                .filter(sheet -> sheet.label.replaceAll("\\s", "").equalsIgnoreCase(spacesRemovedName))
                .findFirst();
        if (!found.isPresent()) {
            try {
                return Optional.of(valueOf(name.toUpperCase()));
            } catch (Exception ex) {
                return Optional.empty();
            }
        }
        return found;
    }

    public String getName() {
        return label;
    }
} @SuppressWarnings("unchecked")
@Log4j
//This class is a @Singleton
public final class CivSingleton {

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

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

    private Cache<String, String> chatCache;

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

    public static CivSingleton instance() {
        return civSingleton;
    }

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

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

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

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

} @Data
@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
@AllArgsConstructor
public class AllTechsDTO {
    private String civ;
    private String color;
    private List<TechDTO> techs;
} @Data
@JsonRootName(value = "forgotpassDTO")
public class ForgotpassDTO {
    @NotEmpty
    @Email
    private String email;

    @NotNull
    private String newpassword;
} @JsonRootName("itemDTO")
@Setter
@Getter
@ToString(of = "name")
@JsonIgnoreProperties(ignoreUnknown = true)
public class ItemDTO {
    private int itemNumber;
    /**
     * ie: Leonidas *
     */
    @NotEmpty
    private String name;
    /**
     * if it is to be sent to a new playerId
     */
    private String ownerId;
    /**
     * If the item is to be revealed
     */
    private boolean hidden;
    private boolean used;
    private String description;
    /**
     * Type of item, ie Scientist
     */
    private String type;
    /**
     * ie Great Person
     */
    @NotNull
    private String sheetName;

    private String pbfId;
} 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
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class TechDTO {
    private String name;
    private int level;
} @Log4j
public class SendEmail {
    public static final String SENDGRID_USERNAME = "SENDGRID_USERNAME";
    public static final String SENDGRID_PASSWORD = "SENDGRID_PASSWORD";
    public static final String NOREPLY_PLAYCIV_COM = "noreply@playciv.com";
    public static final String URL = "http://playciv.com/";
    public static final String REST_URL = "https://civilization-boardgame.herokuapp.com/";
    public static final String CASH_EMAIL = "cash@playciv.com";
    private static final SendGrid sendgrid = new SendGrid(System.getenv(SENDGRID_USERNAME), System.getenv(SENDGRID_PASSWORD));

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

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

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

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

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

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

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

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

        return false;
    }

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

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

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

} public class PlayerExistException extends WebApplicationException {
    public PlayerExistException() {
        super(Response.status(Response.Status.BAD_REQUEST)
                .entity("{\"error:\" \"Player already exists!\"}")
                .type(MediaType.APPLICATION_JSON)
                .build());
    }
} public class SecurityCheck {

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

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

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

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

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

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

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

    @JsonGetter("image")
    @Override
    public String getImage() {
        image = name + PNG;
        return image;
    }
} @NoArgsConstructor
@JsonRootName(value = "draw")
@Data
//TODO Draw is not a good name really. Its really more a UndoableItem
public class Draw<T extends Item> {
    public static final String COL_NAME = "draw";

    /**
     * Typically implementation of Unit or Item. Should have #getSheetName() to determine the type
     */
    @NotNull
    private T item;
    @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 String gameLogId;

    public Draw(String pbfId, String playerId) {
        this.pbfId = pbfId;
        this.playerId = playerId;

        created = LocalDateTime.now();
    }

    /**
     * Returns true if undo has been requested
     */
    @JsonIgnore
    private boolean isUndoInitiated() {
        return undo != null;
    }
} @JsonRootName("gameLog")
@NoArgsConstructor
@ToString(of = {"privateLog", "publicLog"})
@Data
public class GameLog {
    public static final String COL_NAME = "gamelog";
    private static final String DELIM = " - ";
    @Id
    @ObjectId
    private String id;
    private String privateLog;
    private String publicLog;
    /**
     * Each log belongs to a pbf
     */
    @NotEmpty
    private String pbfId;
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime created = LocalDateTime.now();
    @NotEmpty
    private String username;
    /**
     * If log is from a draw
     */
    private Draw draw;

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

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

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

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

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

    public enum LogType {
        TRADE_BETWEEN_PLAYERS, BATTLE, ITEM, TECH, REMOVED_TECH, SHUFFLE, DISCARD, WITHDRAW, JOIN, REVEAL,
        UNDO, SOCIAL_POLICY, REMOVED_SOCIAL_POLICY, VOTE, SETUP, SOT, TRADE, CM, MOVEMENT, RESEARCH;
    }
} public interface Item extends Spreadsheet, Type, Comparable<Spreadsheet> {

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

    public String getOwnerId();

    public void setOwnerId(String owner);

    public boolean isHidden();

    public void setHidden(boolean hidden);

    public boolean isUsed();

    public String getDescription();

    public int getItemNumber();

    public void setItemNumber(int itemNumber);

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

    @ObjectId
    @Id
    private String id;

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

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

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

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

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

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

    private boolean tournament = false;

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

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

        return "";
    }

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

    @ObjectId
    @Id
    private String id;

    @NotBlank
    //Unique
    private String username;

    @Email
    private String email;

    private boolean disableEmail = false;

    @NotBlank
    private String password;

    private String newPassword;

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

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

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

    @NotBlank
    private String playerId;

    private String email;

    private String color;

    private Civ civilization; //The chosen civilization

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

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

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

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

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

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

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

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

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

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

    @Override
    public int compareTo(Playerhand o) {
        return o.getUsername().compareTo(o.getUsername());
    }
} @Getter
@Setter
@ToString()
@JsonTypeName("socialpolicy")
@NoArgsConstructor
@EqualsAndHashCode(of = {"name"})
@JsonIgnoreProperties(ignoreUnknown = true)
public class SocialPolicy implements Item, Image {
    @NotEmpty
    private String name;
    private String type;
    private String description;
    private boolean used;
    private boolean hidden = true;
    private String ownerId;
    private SheetName sheetName;
    private int itemNumber;
    private String image;
    private String flipside;

    public SocialPolicy(String name) {
        this.name = name;
        this.sheetName = SheetName.SOCIAL_POLICY;
        this.used = false;
        this.hidden = true;
    }

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

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

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

    @Override
    public String getImage() {
        image = name + PNG;
        return image.replaceAll(" ", "");
    }
} @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(" ", "");
    }
} public interface Tradable {
} @Setter
public abstract class Unit implements Item, Level, Type {
    @JsonIgnore
    static final int LEVEL_1 = 1;
    @JsonIgnore
    static final int LEVEL_2 = 2;
    @JsonIgnore
    static final int LEVEL_3 = 3;
    @JsonIgnore
    static final int LEVEL_4 = 4;
    private String name;

    public abstract boolean isKilled();

    public abstract int getAttack();

    public abstract int getHealth();

    @Override
    public String getName() {
        return name = getType() + " " + getAttack() + "." + getHealth();
    }

    @JsonIgnore
    @Override
    public String getDescription() {
        return getType();
    }

    @Override
    public String revealPublic() {
        return getType();
    }

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

    public abstract boolean isInBattle();

    public abstract void setInBattle(boolean inBattle);
}
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.mongodb.DB;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j;
import no.asgari.civilization.server.SheetName;
import no.asgari.civilization.server.application.CivSingleton;
import no.asgari.civilization.server.dto.AllTechsDTO;
import no.asgari.civilization.server.dto.ForgotpassDTO;
import no.asgari.civilization.server.dto.ItemDTO;
import no.asgari.civilization.server.dto.MessageDTO;
import no.asgari.civilization.server.dto.TechDTO;
import no.asgari.civilization.server.email.SendEmail;
import no.asgari.civilization.server.exception.PlayerExistException;
import no.asgari.civilization.server.misc.SecurityCheck;
import no.asgari.civilization.server.model.Civ;
import no.asgari.civilization.server.model.Draw;
import no.asgari.civilization.server.model.GameLog;
import no.asgari.civilization.server.model.Item;
import no.asgari.civilization.server.model.PBF;
import no.asgari.civilization.server.model.Player;
import no.asgari.civilization.server.model.Playerhand;
import no.asgari.civilization.server.model.SocialPolicy;
import no.asgari.civilization.server.model.Tech;
import no.asgari.civilization.server.model.Tradable;
import no.asgari.civilization.server.model.Unit;
import org.apache.commons.codec.digest.DigestUtils;
import org.mongojack.DBQuery;
import org.mongojack.JacksonDBCollection;
import org.mongojack.WriteResult;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Response;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/*
 * Copyright (c) 2015 Shervin Asgari
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package no.asgari.civilization.server.action;



@Log4j
public class 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)) {