public class ClockService {
    private LocalTime mockTime = null;

    public ClockService() {
    }

    public void setMockTime(LocalTime mockTime) {
        this.mockTime = mockTime;
    }

    public LocalTime getCurrentTime() {
        return mockTime == null ? LocalTime.now() : mockTime;
    }

    public LocalDateTime getCurrentDateTime() {
        return mockTime == null ? LocalDateTime.now() : LocalDateTime.of(LocalDate.now(), mockTime);
    }
} public class LocaleService {
    private static final Logger LOGGER = LoggerFactory.getLogger(LocaleService.class);
    public static final String NEW_EX_RAID_HELP = "NEW_EX_RAID_HELP";
    public static final String RAIDSTATUS = "RAIDSTATUS";
    public static final String NO_RAID_AT_GYM = "NO_RAID_AT_GYM";
    public static final String REMOVE_SIGNUP_HELP = "REMOVE_SIGNUP_HELP";
    public static final String SIGNUP_REMOVED = "SIGNUP_REMOVED";
    public static final String NO_SIGNUP_AT_GYM = "NO_SIGNUP_AT_GYM";
    public static final String SIGNUP_HELP = "SIGNUP_HELP";
    public static final String ERROR_PARSE_PLAYERS = "ERROR_PARSE_PLAYERS";
    public static final String CURRENT_SIGNUPS = "CURRENT_SIGNUPS";
    public static final String SIGNUPS = "SIGNUPS";
    public static final String WHERE_GYM_HELP = "WHERE_GYM_HELP";
    public static final String ALREADY_SIGNED_UP = "ALREADY_SIGNED_UP";
    public static final String NO_POKEMON = "NO_POKEMON";
    public static final String TIMEZONE = "TIMEZONE";
    public static final String NO_RAIDS_NOW = "NO_RAIDS_NOW";
    public static final String NO_RAID_TOO_LONG = "NO_RAID_TOO_LONG";
    public static final String NO_ETA_AFTER_RAID = "NO_ETA_AFTER_RAID";
    public static final String GYM_SEARCH = "GYM_SEARCH";
    public static final String GYM_SEARCH_OPTIONS = "GYM_SEARCH_OPTIONS";
    public static final String GYM_SEARCH_MANY_RESULTS = "GYM_SEARCH_MANY_RESULTS";
    public static final String GYM_CONFIG_ERROR = "GYM_CONFIG_ERROR";
    public static final String SERVER_HELP = "SERVER_HELP";
    public static final String DONATE = "DONATE";
    public static final String TRACKING_EXISTS = "TRACKING_EXISTS";
    public static final String TRACKED_RAID = "TRACKED_RAID";
    public static final String TRACKING_ADDED = "TRACKING_ADDED";
    public static final String TRACKING_NOT_EXISTS = "TRACKING_NOT_EXISTS";
    public static final String TRACK_HELP = "TRACK_HELP";
    public static final String UNTRACK_HELP = "UNTRACK_HELP";
    public static final String TRACKING_REMOVED = "TRACKING_REMOVED";
    public static final String SIGN_BEFORE_RAID = "SIGN_BEFORE_RAID";
    public static final String GYM_NOT_FOUND = "GYM_NOT_FOUND";
    public static final String RAID_EXISTS = "RAID_EXISTS";
    public static final String USAGE = "USAGE";
    public static final String USAGE_HELP = "USAGE_HELP";
    public static final String AT_YOUR_SERVICE = "AT_YOUR_SERVICE";
    public static final String NEW_RAID_HELP = "NEW_RAID_HELP";
    public static final String NEW_RAID_CREATED = "NEW_RAID_CREATED";
    public static final String RAID_TOSTRING = "RAID_TOSTRING";
    public static final String VS_HELP = "VS_HELP";
    public static final String WEAKNESSES = "WEAKNESSES";
    public static final String RESISTANT = "RESISTANT";
    public static final String BEST_COUNTERS = "BEST_COUNTERS";
    public static final String OTHER_COUNTERS = "OTHER_COUNTERS";
    public static final String IF_CORRECT_MOVESET = "IF_CORRECT_MOVESET";
    public static final String LIST_HELP = "LIST_HELP";
    public static final String LIST_NO_RAIDS = "LIST_NO_RAIDS";
    public static final String RAID_BETWEEN = "RAID_BETWEEN";
    public static final String CURRENT_RAIDS = "CURRENT_RAIDS";
    public static final String SIGNED_UP = "SIGNED_UP";
    public static final String RAIDSTATUS_HELP = "RAIDSTATUS_HELP";
    public static final String GENERIC_USER_ERROR = "GENERIC_USER_ERROR";

    public static final Locale SWEDISH = new Locale("sv");
    public static final String WHERE_GYM_IN_CHAT_HELP = "WHERE_GYM_IN_CHAT_HELP";
    public static final String NEXT_ETA = "NEXT_ETA";
    public static final String TRACKING_NONE_FREE = "TRACKING_NONE_FREE";
    public static final String LAST_UPDATE = "LAST_UPDATE";
    public static final String MOVED_GROUP = "MOVED_GROUP";
    public static final String OVERVIEW_HELP = "OVERVIEW_HELP";
    public static final String OVERVIEW_ATTACH = "OVERVIEW_ATTACH";
    public static final String OVERVIEW_DELETED = "OVERVIEW_DELETED";
    public static final String HELP_USER_CONFIG = "HELP_USER_CONFIG";
    public static final String USER_CONFIG_BAD_SYNTAX = "USER_CONFIG_BAD_SYNTAX";
    public static final String USER_CONFIG_BAD_PARAM = "USER_CONFIG_BAD_PARAM";
    public static final String UNSUPPORTED_LOCALE = "UNSUPPORTED_LOCALE";
    public static final String LOCALE_SET = "LOCALE_SET";
    public static final String MANUAL_CONFIG = "MANUAL_CONFIG";
    public static final String GETTING_STARTED_HELP = "GETTING_STARTED_HELP";
    public static final String PLUS_SIGNUP_FAIL = "PLUS_SIGNUP_FAIL";
    public static final String NO_RAID = "NO_RAID";
    public static final String GROUP_CLEANING_UP = "GROUP_CLEANING_UP";
    public static final String SIGN_BEFORE_NOW = "SIGN_BEFORE_NOW";
    public static final String NOT_EX_RAID = "NOT_EX_RAID";
    public static final String NO_GROUP_BEFORE_RAID = "NO_GROUP_BEFORE_RAID";
    public static final String NEW_RAID_START_HELP = "NEW_RAID_START_HELP";
    public static final String GROUP_NOT_ADDED = "GROUP_NOT_ADDED";
    public static final String MANY_GROUPS_FOR_RAID = "MANY_GROUPS_FOR_RAID";
    public static final String NO_SUCH_GROUP = "NO_SUCH_GROUP";
    public static final String OVERVIEW_EXISTS = "OVERVIEW_EXISTS";
    public static final String TIME_NOT_IN_RAID_TIMESPAN = "TIME_NOT_IN_RAID_TIMESPAN";
    public static final String TOO_MANY_GROUPS = "TOO_MANY_GROUPS";
    public static final String NO_GROUP_BEFORE_NOW = "NO_GROUP_BEFORE_NOW";
    public static final String UNSIGN = "UNSIGN";
    public static final String OVERVIEW_CLEARED = "OVERVIEW_CLEARED";
    public static final String EGG_HATCH_HELP = "EGG_HATCH_HELP";
    public static final String EGG_ALREADY_HATCHED = "EGG_ALREADY_HATCHED";
    public static final String EGG_WRONG_TIER = "EGG_WRONG_TIER";
    public static final String USER_NICK_INVALID = "USER_NICK_INVALID";
    public static final String RAID_CREATE_AND_GROUP_HELP = "RAID_CREATE_AND_GROUP_HELP";
    public static final String GROUP_DELETED = "GROUP_DELETED";
    public static final String CREATED_BY = "CREATED_BY";
    public static final String COULD_NOT_ADD_GYM = "COULD_NOT_ADD_GYM";
    public static final String HELP = "HELP";
    public static final String EX_WITHOUT_RAID = "EX_WITHOUT_RAID";
    public static final String ALL_EX = "ALL_EX";

    // Change this if you want another default locale, affects the usage texts etc
    public static Locale DEFAULT = Locale.ENGLISH;
    public static Locale[] SUPPORTED_LOCALES = {SWEDISH, Locale.ENGLISH};
    public static final String MANUAL_RAID = "MANUAL_RAID";
    public static final String MANUAL_SIGNUP = "MANUAL_SIGNUP";
    public static final String MANUAL_MAP = "MANUAL_MAP";
    public static final String MANUAL_INSTALL = "MANUAL_INSTALL";
    public static final String MANUAL_CHANGE = "MANUAL_CHANGE";
    public static final String WRONG_NUMBER_OF_ARGUMENTS = "WRONG_NUMBER_OF_ARGUMENTS";
    public static final String MANUAL_TRACKING = "MANUAL_TRACKING";
    public static final String ACTIVE = "ACTIVE";
    public static final String START_GROUP = "START_GROUP";
    public static final String FIND_YOUR_WAY = "FIND_YOUR_WAY";
    public static final String RAID_BOSS = "RAID_BOSS";
    public static final String FOR_HINTS = "FOR_HINTS";
    public static final String RAID_GROUP_HELP = "RAID_GROUP_HELP";
    public static final String CANT_CREATE_GROUP_LATE = "CANT_CREATE_GROUP_LATE";
    public static final String HANDLE_SIGNUP = "HANDLE_SIGNUP";
    public static final String REMOVED_GROUP = "REMOVED_GROUP";
    public static final String GROUP_HEADLINE = "GROUP_HEADLINE";
    public static final String POKEMON = "POKEMON";
    public static final String SIGNED_UP_TOTAL = "SIGNED_UP_TOTAL";
    public static final String SIGNED_UP_AT = "SIGNED_UP_AT";
    public static final String NO_EMOTES = "NO_EMOTES";
    public static final String MANUAL_GROUPS = "MANUAL_GROUPS";
    public static final String HELP_MANUAL_HELP_TEXT = "HELP_MANUAL_HELP_TEXT";
    public static final String EX_DATE_LIMITS = "EX_DATE_LIMITS";
    public static final String CHANGE_RAID_HELP = "CHANGE_RAID_HELP";
    public static final String EX_NO_CHANGE_POKEMON = "EX_NO_CHANGE_POKEMON";
    public static final String EX_CANT_CHANGE_RAID_TYPE = "EX_CANT_CHANGE_RAID_TYPE";
    public static final String SIGNUP_BAD_NUMBER = "SIGNUP_BAD_NUMBER";
    public static final String EMOTE_INSTALLED_ALREADY = "EMOTE_INSTALLED_ALREADY";
    public static final String GETTING_HERE = "GETTING_HERE";
    public static final String WHO_ARE_COMING = "WHO_ARE_COMING";
    public static final String ONLY_ADMINS_REMOVE_RAID = "ONLY_ADMINS_REMOVE_RAID";
    public static final String RAID_NOT_EXISTS = "RAID_NOT_EXISTS";
    public static final String BAD_SYNTAX = "BAD_SYNTAX";
    public static final String BAD_DATETIME_FORMAT = "BAD_DATETIME_FORMAT";
    public static final String RAID_DETAILS = "RAID_DETAILS";
    public static final String UPDATED_EVERY_X = "UPDATED_EVERY_X";
    public static final String NO_PERMISSION = "NO_PERMISSION";
    public static final String GOOGLE_MAPS = "GOOGLE_MAPS";
    public static final String KEEP_CHAT_CLEAN = "KEEP_CHAT_CLEAN";
    public static final String ERROR_KEEP_CHAT_CLEAN = "ERROR_KEEP_CHAT_CLEAN";
    public static final String WHATS_NEW_HELP = "WHATS_NEW_HELP";
    public static final String NO_CONFIG = "NO_CONFIG";
    private final UserConfigRepository userConfigRepository;

    private Map<I18nLookup, String> i18nMessages = new HashMap<>();

    public LocaleService(Map<I18nLookup, String> i18nMessages, UserConfigRepository userConfigRepository) {
        this.i18nMessages = i18nMessages;
        this.userConfigRepository = userConfigRepository;
    }

    public LocaleService(String locale, UserConfigRepository userConfigRepository) {
        this.userConfigRepository = userConfigRepository;
        LOGGER.info("Initialize with server default locale: " + locale);
        final Locale forLanguageTag = Locale.forLanguageTag(locale);
        if (!new HashSet<>(Arrays.asList(SUPPORTED_LOCALES)).contains(forLanguageTag)) {
            throw new IllegalStateException("Given system locale " + locale + " is not in list of " +
                    "supported locales (" + SUPPORTED_LOCALES + "). Add it, and associated texts!");
        }
        DEFAULT = forLanguageTag;
        initTexts();
        LOGGER.info("Initialized. Got " + i18nMessages.keySet().size() + " texts for " + SUPPORTED_LOCALES.length + " locales.");
    }

    private void initTexts() {
        i18nMessages.put(new I18nLookup(ALL_EX, Locale.ENGLISH),
                "All EX-gyms for the region ");
        i18nMessages.put(new I18nLookup(ALL_EX, SWEDISH),
                "Alla EX-gym för regionen ");

        i18nMessages.put(new I18nLookup(EX_WITHOUT_RAID, Locale.ENGLISH),
                "Potential EX gyms without a scheduled EX-raid:");
        i18nMessages.put(new I18nLookup(EX_WITHOUT_RAID, SWEDISH),
                "Potentiella EX-gym som inte har en kommande EX-raid:");

        i18nMessages.put(new I18nLookup(HELP, Locale.ENGLISH),
                "Help");
        i18nMessages.put(new I18nLookup(HELP, SWEDISH),
                "Hjälp");

        i18nMessages.put(new I18nLookup(COULD_NOT_ADD_GYM, Locale.ENGLISH),
                "Could not add gym, it already exists for this region.");
        i18nMessages.put(new I18nLookup(COULD_NOT_ADD_GYM, SWEDISH),
                "Kunde inte lägga till gym, det finns redan för den här regionen.");

        i18nMessages.put(new I18nLookup(CREATED_BY, Locale.ENGLISH),
                "Created by"
        );
        i18nMessages.put(new I18nLookup(CREATED_BY, SWEDISH),
                "Skapad av"
        );
        i18nMessages.put(new I18nLookup(GROUP_DELETED, Locale.ENGLISH),
                "Group deleted, including any signups."
        );
        i18nMessages.put(new I18nLookup(GROUP_DELETED, SWEDISH),
                "Grupp borttagen, inklusive alla eventuella anmälningar."
        );
        i18nMessages.put(new I18nLookup(NO_SUCH_GROUP, Locale.ENGLISH),
                "No such group exists."        );
        i18nMessages.put(new I18nLookup(NO_SUCH_GROUP, SWEDISH),
                "Det fanns ingen sådan grupp."
        );

        i18nMessages.put(new I18nLookup(TOO_MANY_GROUPS, Locale.ENGLISH),
                "There is already a group created for this raid by this user. In later releases, we may allow " +
                        "creating many groups per raid per user again."
        );
        i18nMessages.put(new I18nLookup(TOO_MANY_GROUPS, SWEDISH),
                "Det finns redan en grupp för denna raid av denna användare. I framtida releaser, kan vi komma att " +
                        "tillåta att skapa flera grupper per raid per användare igen."
        );
        i18nMessages.put(new I18nLookup(MANY_GROUPS_FOR_RAID, Locale.ENGLISH),
                "There are several groups this user can change for raid %1 - " +
                        "Use *!raid change group remove {group starttime} {gym}* to remove the group."
        );
        i18nMessages.put(new I18nLookup(MANY_GROUPS_FOR_RAID, SWEDISH),
                "Det finns flera grupper denna användare kan ändra för raiden %1 - " +
                        "använd *!raid change group remove {grupptid} {gym}* för att ta bort gruppen."
        );
        i18nMessages.put(new I18nLookup(TIME_NOT_IN_RAID_TIMESPAN, Locale.ENGLISH),
                "There were several possible groups to change, so couldn't perform this change. " +
                        "Workaround: remove the group's discord messages and create a new group with the correct time."
        );
        i18nMessages.put(new I18nLookup(TIME_NOT_IN_RAID_TIMESPAN, SWEDISH),
                "Det fanns flera möjliga grupper att ändra på, så kan inte göra detta. " +
                        "Workaround: ta bort gruppens meddelanden och skapa en ny grupp med rätt tid."
        );
        i18nMessages.put(new I18nLookup(TIME_NOT_IN_RAID_TIMESPAN, Locale.ENGLISH),
                "Time %1 is not within the timespan of the raid (%2 - %3)"
        );
        i18nMessages.put(new I18nLookup(TIME_NOT_IN_RAID_TIMESPAN, SWEDISH),
                "Tiden %1 är inte inom raidens giltiga tid (%2 - %3)"
        );

        i18nMessages.put(new I18nLookup(OVERVIEW_CLEARED, Locale.ENGLISH),
                "Server overview message ID cleared."
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_CLEARED, SWEDISH),
                "Serverns översiktmeddelande rensat från konfigurationen."
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_EXISTS, Locale.ENGLISH),
                "Server already has an overview command. Don't run this command again," +
                        " except if the overview message stops updating (saved ID can be cleared via *!raid overview reset*)."
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_EXISTS, SWEDISH),
                "Servern har redan en översikt. Kör inte detta kommando igen, om inte översikten slutar uppdateras" +
                        " (sparad ID kan rensas via *!raid overview reset*)."
        );
        i18nMessages.put(new I18nLookup(GROUP_NOT_ADDED, Locale.ENGLISH),
                "There is already a raidgroup at this time for %1"
        );
        i18nMessages.put(new I18nLookup(GROUP_NOT_ADDED, SWEDISH),
                "Det finns redan en grupp för denna tid för %1"
        );
        i18nMessages.put(new I18nLookup(EGG_ALREADY_HATCHED, Locale.ENGLISH),
                "This raid has already been reported as hatched, the pokemon is %1. " +
                        "If it's wrong, change via *!raid change pokemon {pokemon name} {gym}*.");
        i18nMessages.put(new I18nLookup(EGG_ALREADY_HATCHED, SWEDISH),
                "Den här raiden har redan rapporterats som kläckt, bossen är %1. " +
                        "Om det inte stämmer, ändra via *!raid change pokemon {pokemon name} {gym}* " +
                        "(admins eller raidskapare).");
        i18nMessages.put(new I18nLookup(EGG_WRONG_TIER, Locale.ENGLISH),
                "Attempt to hatch a raid boss of a different tier than the reported egg!");
        i18nMessages.put(new I18nLookup(EGG_WRONG_TIER, SWEDISH),
                "Försök att kläcka en raidboss av annan nivå än det rapporterade ägget! Om det är fel typ av ägg, " +
        "ändra via *!raid change pokemon Egg{rätt nivå} {gym}* ..");

        i18nMessages.put(new I18nLookup(EGG_HATCH_HELP, Locale.ENGLISH),
                "Report the hatch of a reported egg - !raid hatch [Name of Pokemon] [Gym name]");
        i18nMessages.put(new I18nLookup(EGG_HATCH_HELP, SWEDISH),
                "Rapportera att ett rapporterat ägg kläckts - !raid hatch [Pokemon] [Gym]");
        i18nMessages.put(new I18nLookup(NEW_RAID_START_HELP, Locale.ENGLISH),
                "Create new raid starting at time - !raid start [Name of Pokemon] [Start (HH:MM)] [Gym name]");
        i18nMessages.put(new I18nLookup(NEW_RAID_START_HELP, SWEDISH),
                "Skapa ny raid som startar vid viss tid - !raid start [Pokemon] [Start klockan (HH:MM)] [Gym]");
        i18nMessages.put(new I18nLookup(RAID_CREATE_AND_GROUP_HELP, Locale.ENGLISH),
                "Create new raid and group starting at time - !raid start-group [Name of Pokemon] " +
                        "[Start (HH:MM)] [Gym name]");
        i18nMessages.put(new I18nLookup(RAID_CREATE_AND_GROUP_HELP, SWEDISH),
                "Skapa ny raid och grupp som startar vid viss tid - !raid start-group [Pokemon] " +
                        "[Start klockan (HH:MM)] [Gym]");
        i18nMessages.put(new I18nLookup(NO_GROUP_BEFORE_NOW, Locale.ENGLISH),
                "Can't set a raid group to start at %1, which is before current time %2."
        );
        i18nMessages.put(new I18nLookup(NO_GROUP_BEFORE_NOW, SWEDISH),
                "Kan inte sätta en raidgrupp att börja vid %1, eftersom klockan är %2."
        );
        i18nMessages.put(new I18nLookup(NO_GROUP_BEFORE_RAID, Locale.ENGLISH),
                "Can't set a raid group to start at %1, which is before raid start at %2."
        );
        i18nMessages.put(new I18nLookup(NO_GROUP_BEFORE_RAID, SWEDISH),
                "Kan inte sätta en raidgrupp att börja vid %1, eftersom raiden börjar %2."
        );
        i18nMessages.put(new I18nLookup(NOT_EX_RAID, Locale.ENGLISH),
                "%1 is not an EX raid boss. Use *!raid new* command instead to create a standard raid."
        );
        i18nMessages.put(new I18nLookup(NOT_EX_RAID, SWEDISH),
                "%1 är inte en EX raid boss. Använd *!raid new* istället för att skapa en vanlig raid."
        );
        i18nMessages.put(new I18nLookup(GROUP_CLEANING_UP, Locale.ENGLISH),
                "This group is about to be removed. " +
                        "Create a new group via *!raid group {time (HH:MM)} {gym name}*"
        );
        i18nMessages.put(new I18nLookup(GROUP_CLEANING_UP, SWEDISH),
                "Detta meddelande är på gång att städas undan. " +
                        "Skapa en ny grupp via *!raid group {tid (HH:MM)} {gym}*"
        );
        i18nMessages.put(new I18nLookup(GETTING_STARTED_HELP, Locale.ENGLISH),
                "Getting started guide for the bot"
        );
        i18nMessages.put(new I18nLookup(GETTING_STARTED_HELP, SWEDISH),
                "Kom-igång-guide för pokeraidbot"
        );
        i18nMessages.put(new I18nLookup(LOCALE_SET, Locale.ENGLISH),
                "Locale set to: %1"
        );
        i18nMessages.put(new I18nLookup(LOCALE_SET, SWEDISH),
                "Locale (språk) satt till: %1"
        );
        i18nMessages.put(new I18nLookup(UNSUPPORTED_LOCALE, Locale.ENGLISH),
                "You tried to set an unsupported locale: %1. Supported locales are: " +
                        StringUtils.join(LocaleService.SUPPORTED_LOCALES, ", ")
        );
        i18nMessages.put(new I18nLookup(UNSUPPORTED_LOCALE, SWEDISH),
                "Du försökte sätta en locale som inte stödjs: %1. Tillgängliga locales är: " +
                        StringUtils.join(LocaleService.SUPPORTED_LOCALES, ", ")
        );

        i18nMessages.put(new I18nLookup(USER_NICK_INVALID, Locale.ENGLISH),
                "Invalid nickname, needs to be between 2 and 11 chars."
        );
        i18nMessages.put(new I18nLookup(USER_NICK_INVALID, SWEDISH),
                "Ogiltigt smeknamn, ska vara mellan 2 och 11 tecken."
        );
        i18nMessages.put(new I18nLookup(USER_CONFIG_BAD_PARAM, Locale.ENGLISH),
                "The only parameters that can be changed right now via this command is locale and nick. " +
                        "You tried to set %1."
        );
        i18nMessages.put(new I18nLookup(USER_CONFIG_BAD_PARAM, SWEDISH),
                "De enda parametrarna som kan ändras just nu via detta kommando är språk (locale) och smeknamn. " +
                        "Du försökte sätta parametern %1."
        );
        i18nMessages.put(new I18nLookup(USER_CONFIG_BAD_SYNTAX, Locale.ENGLISH),
                "Bad syntax. To see user's configuration: *!raid config show*\n" +
                        "To change: *!raid config {param}={value}*"
        );
        i18nMessages.put(new I18nLookup(USER_CONFIG_BAD_SYNTAX, SWEDISH),
                "Felaktigt kommando. För att visa användarens konfiguration: *!raid config show*\n" +
                        "För att ändra den: *!raid config {param}={value}*"
        );
        i18nMessages.put(new I18nLookup(HELP_USER_CONFIG, Locale.ENGLISH),
                "Get or change user configuration - !raid config show to display, " +
                        "!raid config {param}={value} to change."
        );
        i18nMessages.put(new I18nLookup(HELP_USER_CONFIG, SWEDISH),
                "Användarkonfiguration - !raid config show för att visa, " +
                        "!raid config {param}={value} för att ändra."
        );
        i18nMessages.put(new I18nLookup(MOVED_GROUP, Locale.ENGLISH),
                "Moved group from %1 to %2 for raid at %3.\n" +
                        "**Note: all signups for this time are moved to the new time.** " +
                        "Users who don't want to be moved have to run " +
                        "*!raid remove %3* and then add their signup again."
        );
        i18nMessages.put(new I18nLookup(MOVED_GROUP, SWEDISH),
                "Flyttade grupp från %1 till %2 för raid vid %3.\n" +
                        "**OBS: Alla gjorda anmälningar för denna tid flyttas med.** " +
                        "Vill du inte det - ändra din anmälning, t.ex. via *!raid remove %3* och lägg till dig igen."
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_DELETED, Locale.ENGLISH),
                "Overview message has been removed by someone. " +
                        "Run *!raid overview* again to create a new one."
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_DELETED, SWEDISH),
                "Översiktsmeddelandet har tagits bort av någon. " +
                        "Kör *!raid overview* igen för att skapa ett nytt."
        );
        i18nMessages.put(new I18nLookup(LAST_UPDATE, Locale.ENGLISH),
                "Last update: %1"
        );
        i18nMessages.put(new I18nLookup(LAST_UPDATE, SWEDISH),
                "Senast uppdaterad: %1"
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_ATTACH, Locale.ENGLISH),
                "Pokeraidbot v" + BotServerMain.version + " is here. Raid overview will be updated in channel " +
                        "#%1. For bot info, type: *!raid usage*"
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_ATTACH, SWEDISH),
                "Pokeraidbot v" + BotServerMain.version + " är här. Raidöversikten uppdateras i kanalen " +
                        "#%1. För info om botten: *!raid usage*"
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_HELP, Locale.ENGLISH),
                "Create a message with a !raid list overview that automatically updates: !raid overview"
        );
        i18nMessages.put(new I18nLookup(OVERVIEW_HELP, SWEDISH),
                "Skapa ett meddelande med en !raid list översikt som automatiskt uppdateras: !raid overview"
        );

        i18nMessages.put(new I18nLookup(TRACKING_NONE_FREE, Locale.ENGLISH),
                "There are no free spots for pokemon tracking in your user configuration (3 spots filled). " +
                        "Please remove one or more via *!raid untrack {pokemon}*"
        );
        i18nMessages.put(new I18nLookup(TRACKING_NONE_FREE, SWEDISH),
                "Alla dina 3 trackingpokemon är upptagna. Ta bort en eller fler via *!raid untrack*"
        );

        i18nMessages.put(new I18nLookup(NEXT_ETA, Locale.ENGLISH),
                "next ETA: %1"
        );
        i18nMessages.put(new I18nLookup(NEXT_ETA, SWEDISH),
                "närmaste ETA: %1"
        );

        i18nMessages.put(new I18nLookup(NO_CONFIG, Locale.ENGLISH),
                "There is no configuration setup for this server. Make sure an administrator runs the command" +
                        " \"!raid install\"."
        );
        i18nMessages.put(new I18nLookup(NO_CONFIG, SWEDISH),
                "Det finns ingen konfiguration installerad för denna server. " +
                        "Se till att en administratör kör kommandot \"!raid install\"."
        );

        i18nMessages.put(new I18nLookup(WHATS_NEW_HELP, Locale.ENGLISH),
                "Gives the version of the bot and what's new in this version."
        );
        i18nMessages.put(new I18nLookup(WHATS_NEW_HELP, SWEDISH),
                "Ange vilken version av botten som körs, och vad som är nytt i denna version."
        );
        i18nMessages.put(new I18nLookup(ERROR_KEEP_CHAT_CLEAN, Locale.ENGLISH),
                "This message, and the associated error, will be removed in %1 seconds to keep chat clean."
        );
        i18nMessages.put(new I18nLookup(ERROR_KEEP_CHAT_CLEAN, SWEDISH),
                "Detta meddelande och tillhörande fel kommer tas bort om %1 sekunder " +
                        "för att hålla chatten ren."
        );
        i18nMessages.put(new I18nLookup(KEEP_CHAT_CLEAN, Locale.ENGLISH),
                "This message will be removed in %1 seconds to keep chat clean."
        );
        i18nMessages.put(new I18nLookup(KEEP_CHAT_CLEAN, SWEDISH),
                "Detta meddelande kommer tas bort om %1 sekunder " +
                        "för att hålla chatten ren."
        );
        i18nMessages.put(new I18nLookup(GOOGLE_MAPS, Locale.ENGLISH),
                " click the message title for a Google Maps link."
        );
        i18nMessages.put(new I18nLookup(GOOGLE_MAPS, SWEDISH),
                " klicka på meddelandetitel för Google Maps."
        );
        i18nMessages.put(new I18nLookup(NO_PERMISSION, Locale.ENGLISH),
                "You lack the permissions to do what you're trying to do. Ask an admin for help."
        );
        i18nMessages.put(new I18nLookup(NO_PERMISSION, SWEDISH),
                "Du saknar behörighet för att göra det du försökte göra. Be hjälp av en admin."
        );
        i18nMessages.put(new I18nLookup(UPDATED_EVERY_X, Locale.ENGLISH),
                "Updated every %2 %1."
        );
        i18nMessages.put(new I18nLookup(UPDATED_EVERY_X, SWEDISH),
                "Uppdateras var %2:e %1."
        );

        i18nMessages.put(new I18nLookup(RAID_DETAILS, Locale.ENGLISH),
                "To see details for a raid: !raid status {gym name}"
        );
        i18nMessages.put(new I18nLookup(RAID_DETAILS, SWEDISH),
                "För att se detaljer för en raid: !raid status {gym-namn}"
        );

        i18nMessages.put(new I18nLookup(BAD_DATETIME_FORMAT, Locale.ENGLISH),
                "Could not parse your given time, should be format %1 but was: %2"
        );
        i18nMessages.put(new I18nLookup(BAD_DATETIME_FORMAT, SWEDISH),
                "Kunde inte tolka tiden du angav, ska vara format %1 men var: %2"
        );

        i18nMessages.put(new I18nLookup(BAD_SYNTAX, Locale.ENGLISH),
                "Bad syntax for command. Refer to *!raid man*. Correct command: %1"
        );
        i18nMessages.put(new I18nLookup(BAD_SYNTAX, SWEDISH),
                "Dålig syntax för kommandot. Se *!raid man* vid behov. Korrekt kommando ser ut som: %1"
        );

        i18nMessages.put(new I18nLookup(RAID_NOT_EXISTS, Locale.ENGLISH),
                "Could not delete raid since you tried to delete one that doesn't exist."
        );
        i18nMessages.put(new I18nLookup(RAID_NOT_EXISTS, SWEDISH),
                "Kunde inte ta bort raid, eftersom den fanns inte."        );

        i18nMessages.put(new I18nLookup(ONLY_ADMINS_REMOVE_RAID, Locale.ENGLISH),
                "Only admins can remove raids with signups, sorry."
        );
        i18nMessages.put(new I18nLookup(ONLY_ADMINS_REMOVE_RAID, SWEDISH),
                "Bara administratörer kan ta bort raids med gjorda anmälningar, tyvärr."
        );
        i18nMessages.put(new I18nLookup(WHO_ARE_COMING, Locale.ENGLISH),
                "Who are coming"
        );
        i18nMessages.put(new I18nLookup(WHO_ARE_COMING, SWEDISH),
                "Vilka kommer"
        );

        i18nMessages.put(new I18nLookup(GETTING_HERE, Locale.ENGLISH),
                "Getting here:"
        );
        i18nMessages.put(new I18nLookup(GETTING_HERE, SWEDISH),
                "Hitta hit:"
        );
        i18nMessages.put(new I18nLookup(EMOTE_INSTALLED_ALREADY, Locale.ENGLISH),
                "You already have an icon with the name \"%1\"."
        );
        i18nMessages.put(new I18nLookup(EMOTE_INSTALLED_ALREADY, SWEDISH),
                "Du har redan installerat emote för: \"%1\"."
        );
        i18nMessages.put(new I18nLookup(SIGNUP_BAD_NUMBER, Locale.ENGLISH),
                "Number of people for a signup must be 1-20, you had %1 but tried to set %2."
        );
        i18nMessages.put(new I18nLookup(SIGNUP_BAD_NUMBER, SWEDISH),
                "Antal personer för en signup måste vara 1-20, du hade %1 men försökte sätta %2."
        );
        i18nMessages.put(new I18nLookup(EX_CANT_CHANGE_RAID_TYPE, Locale.ENGLISH),
                "Can't change a standard raid to be an EX raid. " +
                        "Remove the standard raid and then create an EX raid instead. Refer to !raid man change"
        );
        i18nMessages.put(new I18nLookup(EX_CANT_CHANGE_RAID_TYPE, SWEDISH),
                "Kan inte ändra en vanlig raid till att bli en EX raid. " +
                        "Ta bort den vanliga raiden och skapa en ny EX raid. Använd !raid man change"
        );
        i18nMessages.put(new I18nLookup(EX_NO_CHANGE_POKEMON, Locale.ENGLISH),
                "Can't change pokemon for an EX raid. If you want to change an EX raid, remove it and create a new " +
                        "raid via: !raid change remove {gym}"
        );
        i18nMessages.put(new I18nLookup(EX_NO_CHANGE_POKEMON, SWEDISH),
                "Kan inte ändra pokemon för en EX raid. " +
                        "Om du vill ändra EX raiden, ta bort den och skapa en ny. Använd !raid man change"
        );
        i18nMessages.put(new I18nLookup(CHANGE_RAID_HELP, Locale.ENGLISH),
                " Change something that went wrong during raid creation. Type \"!raid man change\" for details."
        );
        i18nMessages.put(new I18nLookup(CHANGE_RAID_HELP, SWEDISH),
                " Ändra något som blev fel vid skapandet av en raid. Skriv \"!raid man change\" för detaljer."
        );

        i18nMessages.put(new I18nLookup(EX_DATE_LIMITS, Locale.ENGLISH),
                "You can't create an EX raid more than 10 days ahead."
        );
        i18nMessages.put(new I18nLookup(EX_DATE_LIMITS, SWEDISH),
                "Du kan inte skapa en EX raid mer än 10 dagar framåt tyvärr."
        );

        i18nMessages.put(new I18nLookup(NO_EMOTES, Locale.ENGLISH),
                "Administrator has not installed pokeraidbot's emotes. " +
                        "Ensure he/she runs the following command: !raid install-emotes"
        );
        i18nMessages.put(new I18nLookup(NO_EMOTES, SWEDISH),
                "Administratören för denna server har inte installerat pokeraidbot's emotes. " +
                        "Se till att hen kör följande kommando: !raid install-emotes"
        );

        i18nMessages.put(new I18nLookup(SIGNED_UP_AT, Locale.ENGLISH),
                "Signed up"
        );
        i18nMessages.put(new I18nLookup(SIGNED_UP_AT, SWEDISH),
                "Anmälda"
        );

        i18nMessages.put(new I18nLookup(SIGNED_UP_TOTAL, Locale.ENGLISH),
                "Signed up for raid"
        );
        i18nMessages.put(new I18nLookup(SIGNED_UP_TOTAL, SWEDISH),
                "Anmälda totalt till raiden"
        );

        i18nMessages.put(new I18nLookup(POKEMON, Locale.ENGLISH),
                "Pokemon:"
        );
        i18nMessages.put(new I18nLookup(POKEMON, SWEDISH),
                "Pokemon:"
        );

        i18nMessages.put(new I18nLookup(GROUP_HEADLINE, Locale.ENGLISH),
                "%1 @ %2" //, starts at %3"
        );
        i18nMessages.put(new I18nLookup(GROUP_HEADLINE, SWEDISH),
                "%1 @ %2" //, startar %3"
        );

        i18nMessages.put(new I18nLookup(HANDLE_SIGNUP, Locale.ENGLISH),
                "To sign up, press emotes below for number of people to sign up.");
        i18nMessages.put(new I18nLookup(HANDLE_SIGNUP, SWEDISH),
                "För anmälan, tryck emotes nedan motsvarande antal som kommer.");

        i18nMessages.put(new I18nLookup(CANT_CREATE_GROUP_LATE, Locale.ENGLISH),
                "Can't create a group to raid after raid has ended. :("
        );
        i18nMessages.put(new I18nLookup(CANT_CREATE_GROUP_LATE, SWEDISH),
                "Kan inte skapa en grupp som ska samlas efter att raiden slutat."
        );

        i18nMessages.put(new I18nLookup(RAID_GROUP_HELP, Locale.ENGLISH),
                "Create a group which will start the raid together at a given time: " +
                        "!raid group [start time (HH:MM)] [gym name]");
        i18nMessages.put(new I18nLookup(RAID_GROUP_HELP, SWEDISH),
                "Skapa ett tillfälle för en grupp att köra vid en skapad raid: " +
                        "!raid group [start time (HH:MM)] [gym name]");

        i18nMessages.put(new I18nLookup(FOR_HINTS, Locale.ENGLISH), "For hints - type:");
        i18nMessages.put(new I18nLookup(FOR_HINTS, SWEDISH), "För tips - skriv:");

        i18nMessages.put(new I18nLookup(RAID_BOSS, Locale.ENGLISH), "Raidboss:");
        i18nMessages.put(new I18nLookup(RAID_BOSS, SWEDISH), "Raidboss:");

        i18nMessages.put(new I18nLookup(FIND_YOUR_WAY, Locale.ENGLISH), "Find your way here:");
        i18nMessages.put(new I18nLookup(FIND_YOUR_WAY, SWEDISH), "Hitta dit:");

        i18nMessages.put(new I18nLookup(START_GROUP, Locale.ENGLISH), "Start group - write (change the time)");
        i18nMessages.put(new I18nLookup(START_GROUP, SWEDISH), "Starta grupp - skriv (med egen tid)");

        i18nMessages.put(new I18nLookup(ACTIVE, Locale.ENGLISH), "Active");
        i18nMessages.put(new I18nLookup(ACTIVE, SWEDISH), "Aktiv");

        i18nMessages.put(new I18nLookup(WRONG_NUMBER_OF_ARGUMENTS, Locale.ENGLISH),
                "Wrong number of arguments for command, expected %1 but was %2. Write \"!raid man\" for help.");
        i18nMessages.put(new I18nLookup(WRONG_NUMBER_OF_ARGUMENTS, SWEDISH),
                "Fel antal argument, förväntade %1, men det var %2. " +
                        "Skriv \"!raid man\" för att få hjälp");

        i18nMessages.put(new I18nLookup(UNTRACK_HELP, Locale.ENGLISH),
                "Remove an active Pokemon tracking - !raid untrack [Pokemon]");
        i18nMessages.put(new I18nLookup(UNTRACK_HELP, SWEDISH),
                "Ta bort övervakning för viss Pokemon - !raid untrack [Pokemon]");

        i18nMessages.put(new I18nLookup(TRACK_HELP, Locale.ENGLISH),
                "Track new raids for a certain Pokemon (message in DM) - !raid track [Pokemon]");
        i18nMessages.put(new I18nLookup(TRACK_HELP, SWEDISH),
                "Håll koll efter nya raider för en viss Pokemon (via DM) - !raid track [Pokemon]");

        i18nMessages.put(new I18nLookup(TRACKING_ADDED, Locale.ENGLISH),
                "Added tracking for pokemon %1 for user %2.");
        i18nMessages.put(new I18nLookup(TRACKING_ADDED, SWEDISH),
                "Lade till övervakning av pokemon %1 för %2.");

        i18nMessages.put(new I18nLookup(TRACKING_REMOVED, Locale.ENGLISH),
                "Removed tracking for %1 for user %2.");
        i18nMessages.put(new I18nLookup(TRACKING_REMOVED, SWEDISH),
                "Tog bort övervakning av %1 för %2.");

        i18nMessages.put(new I18nLookup(TRACKED_RAID, Locale.ENGLISH),
                "%3 by %2- raid track notification for server %1");
        i18nMessages.put(new I18nLookup(TRACKED_RAID, SWEDISH),
                "%3 av %2 - notifikation från %1");

       i18nMessages.put(new I18nLookup(TRACKING_NOT_EXISTS, Locale.ENGLISH),
                "There was no such tracking set for you.");
        i18nMessages.put(new I18nLookup(TRACKING_NOT_EXISTS, SWEDISH),
                "Det fanns ingen sådan övervakning för dig.");

        i18nMessages.put(new I18nLookup(TRACKING_EXISTS, Locale.ENGLISH),
                "You're already tracking that pokemon.");
        i18nMessages.put(new I18nLookup(TRACKING_EXISTS, SWEDISH),
                "Du har redan övervakning satt för det.");

        i18nMessages.put(new I18nLookup(DONATE, Locale.ENGLISH),
                "How to support development and maintenance of this bot via donating.");
        i18nMessages.put(new I18nLookup(DONATE, SWEDISH),
                "Hur kan man stödja utveckling och drift av botten?");

        i18nMessages.put(new I18nLookup(GYM_CONFIG_ERROR, Locale.ENGLISH),
                "There are no gyms for this region. " +
                        "Please check configuration and/or notify administrator! You have imported the gymdata, right?");
        i18nMessages.put(new I18nLookup(GYM_CONFIG_ERROR, SWEDISH),
                "Det finns inga gym för din valda region. Meddela en administratör så de kan kontrollera " +
                        "konfigurationen av servern. Ni har väl sett till att importera gymdata?");
        i18nMessages.put(new I18nLookup(GYM_SEARCH_MANY_RESULTS, Locale.ENGLISH),
                "Could not find one unique gym/pokestop, your query returned 5+ results. Try refine your search.");
        i18nMessages.put(new I18nLookup(GYM_SEARCH_MANY_RESULTS, SWEDISH),
                "Kunde inte hitta ett unikt gym/pokestop, din sökning returnerade mer än 5 resultat. " +
                        "Försök vara mer precis.");

        i18nMessages.put(new I18nLookup(GYM_SEARCH_OPTIONS, Locale.ENGLISH),
                "Could not find one unique gym/pokestop. Did you want any of these? %1");
        i18nMessages.put(new I18nLookup(GYM_SEARCH_OPTIONS, SWEDISH),
                "Kunde inte hitta ett unikt gym/pokestop. Var det något av dessa du sökte efter? %1");

        i18nMessages.put(new I18nLookup(GYM_SEARCH, Locale.ENGLISH),
                "Empty input for gym name, try giving me a proper name to search for.");
        i18nMessages.put(new I18nLookup(GYM_SEARCH, SWEDISH),
                "Tom söksträng för gymnamn, ge mig något skoj att söka efter!");

        i18nMessages.put(new I18nLookup(SIGN_BEFORE_NOW, Locale.ENGLISH),
                "Can't sign up for this raid before current time. Your given time is %1, time is currently %2");
        i18nMessages.put(new I18nLookup(SIGN_BEFORE_NOW, SWEDISH),
                "Du kan inte anmäla dig att anlända innan nuvarande tid. Din ETA är %1, men klockan är %2.");

        i18nMessages.put(new I18nLookup(SIGN_BEFORE_RAID, Locale.ENGLISH),
                "Can't sign up for this raid before raid start. Your given time is %1, raid start is %2");
        i18nMessages.put(new I18nLookup(SIGN_BEFORE_RAID, SWEDISH),
                "Du kan inte anmäla dig att anlända innan raiden börjar. Din ETA är %1, men raiden börjar %2.");

        i18nMessages.put(new I18nLookup(NO_ETA_AFTER_RAID, Locale.ENGLISH),
                "Can't arrive after raid has ended. Your given time is %1, raid ends at %2");
        i18nMessages.put(new I18nLookup(NO_ETA_AFTER_RAID, SWEDISH),
                "Det är väl inte så lämpligt att anlända efter att raiden slutat? Din ETA är %1, raiden slutar %2.");

        i18nMessages.put(new I18nLookup(NO_RAID_TOO_LONG, Locale.ENGLISH),
                "You can't set an end of raid time which is later than %3 hours from the current time " +
                        "%2 except for EX raids - your input would have yielded end time %1.");
        i18nMessages.put(new I18nLookup(NO_RAID_TOO_LONG, SWEDISH),
                "Du kan inte sätta en sluttid för raid senare än %3 timmar från vad klockan är nu (%2), " +
                        "förutom för EX raider. Rapporterad sluttid skulle blivit %1.");

        i18nMessages.put(new I18nLookup(NO_RAIDS_NOW, Locale.ENGLISH),
                "You can't create raids between 22:00 and 06:00 - your time was %1.");
        i18nMessages.put(new I18nLookup(NO_RAIDS_NOW, SWEDISH),
                "Du kan inte skapa en raid som slutar mellan 22.00 och 06:00. Du angav %1.");

        i18nMessages.put(new I18nLookup(TIMEZONE, Locale.ENGLISH),
                "You seem to be living in a different timezone. Your input was %1, while it's currently %2.");
        i18nMessages.put(new I18nLookup(TIMEZONE, SWEDISH),
                "Fel tidszon? " +
                        "Du angav %1, och klockan är just nu %2.");

        i18nMessages.put(new I18nLookup(NO_POKEMON, Locale.ENGLISH), "Could not find a pokemon with name \"%1\".");
        i18nMessages.put(new I18nLookup(NO_POKEMON, SWEDISH), "Kunde inte hitta pokemon med namn \"%1\".");

        i18nMessages.put(new I18nLookup(ALREADY_SIGNED_UP, Locale.ENGLISH), "You're already signed up for: %1 ... " +
                "%2 - remove your current signup then signup again.");
        i18nMessages.put(new I18nLookup(ALREADY_SIGNED_UP, SWEDISH), "Du har redan anmält dig till raid vid %1 ... " +
                "%2 - ta bort din anmälan och gör om som du vill ha det.");

        i18nMessages.put(new I18nLookup(WHERE_GYM_IN_CHAT_HELP, Locale.ENGLISH), "Get map link for gym in chat - !raid mapinchat [Gym name]");
        i18nMessages.put(new I18nLookup(WHERE_GYM_IN_CHAT_HELP, SWEDISH), "Visa karta för gym i chatten - !raid mapinchat [Gym]");

        i18nMessages.put(new I18nLookup(WHERE_GYM_HELP, Locale.ENGLISH), "Get map link for gym - !raid map [Gym name]");
        i18nMessages.put(new I18nLookup(WHERE_GYM_HELP, SWEDISH), "Visa karta för gym - !raid map [Gym]");

        i18nMessages.put(new I18nLookup(SIGNUPS, Locale.ENGLISH), "%1 sign up added to %2. %3");
        i18nMessages.put(new I18nLookup(SIGNUPS, SWEDISH), "Anmälan från %1 registrerad till %2. %3");

        i18nMessages.put(new I18nLookup(UNSIGN, Locale.ENGLISH), "%1 unsign from %2. %3");
        i18nMessages.put(new I18nLookup(UNSIGN, SWEDISH), "%1 avanmälde från %2. %3");

        i18nMessages.put(new I18nLookup(CURRENT_SIGNUPS, Locale.ENGLISH), "Current signups: ");
        i18nMessages.put(new I18nLookup(CURRENT_SIGNUPS, SWEDISH), "Vilka kommer: ");

        i18nMessages.put(new I18nLookup(ERROR_PARSE_PLAYERS, Locale.ENGLISH),
                "Can't parse this number of people: %1 - give a valid number 1-%2.");
        i18nMessages.put(new I18nLookup(ERROR_PARSE_PLAYERS, SWEDISH),
                "Felaktigt antal personer: %1 - ange ett korrekt antal 1-%2.");

        i18nMessages.put(new I18nLookup(SERVER_HELP, Locale.ENGLISH),
                "Info about your server's configuration: !raid server");
        i18nMessages.put(new I18nLookup(SERVER_HELP, SWEDISH),
                "Information om serverns konfiguration: !raid server");

        i18nMessages.put(new I18nLookup(SIGNUP_HELP, Locale.ENGLISH),
                "Sign up for a raid: !raid add [number of people] [ETA (HH:MM)] [Gym]");
        i18nMessages.put(new I18nLookup(SIGNUP_HELP, SWEDISH),
                "Anmäl dig till en raid: !raid add [antal spelare] [ETA (HH:MM)] [Gym]");

        i18nMessages.put(new I18nLookup(NO_SIGNUP_AT_GYM, Locale.ENGLISH), "%1 had no signup to remove for gym %2");
        i18nMessages.put(new I18nLookup(NO_SIGNUP_AT_GYM, SWEDISH), "%1 hade ingen anmälan att ta bort för raid vid %2");

        i18nMessages.put(new I18nLookup(SIGNUP_REMOVED, Locale.ENGLISH), "Signup removed for gym %1: %2");
        i18nMessages.put(new I18nLookup(SIGNUP_REMOVED, SWEDISH), "Tog bort din anmälan för gym %1: %2");

        i18nMessages.put(new I18nLookup(REMOVE_SIGNUP_HELP, Locale.ENGLISH),
                "Remove your signup for this gym: !raid remove [Gym]");
        i18nMessages.put(new I18nLookup(REMOVE_SIGNUP_HELP, SWEDISH),
                "Ta bort din anmälan för raid på ett gym: !raid remove [Gym]");

        i18nMessages.put(new I18nLookup(NO_RAID_AT_GYM, Locale.ENGLISH),
                "Could not find a raid for this gym: \"%1\".");
        i18nMessages.put(new I18nLookup(NO_RAID_AT_GYM, SWEDISH),
                "Kunde inte hitta någon aktuell raid för \"%1\".");

        i18nMessages.put(new I18nLookup(NO_RAID, Locale.ENGLISH),
                "Could not find the target raid.");
        i18nMessages.put(new I18nLookup(NO_RAID, SWEDISH),
                "Kunde inte hitta den aktuella raiden.");

        i18nMessages.put(new I18nLookup(RAIDSTATUS, Locale.ENGLISH), "%1:");
        i18nMessages.put(new I18nLookup(RAIDSTATUS, SWEDISH), "%1:");

        i18nMessages.put(new I18nLookup(RAIDSTATUS_HELP, Locale.ENGLISH),
                "Check status for raid - !raid status [Gym name].");
        i18nMessages.put(new I18nLookup(RAIDSTATUS_HELP, SWEDISH),
                "Se status för raid - !raid status [Gym].");

        i18nMessages.put(new I18nLookup(SIGNED_UP, Locale.ENGLISH), "Signed up");
        i18nMessages.put(new I18nLookup(SIGNED_UP, SWEDISH), "Anmäld(a)");

        i18nMessages.put(new I18nLookup(CURRENT_RAIDS, Locale.ENGLISH), "Current raids");
        i18nMessages.put(new I18nLookup(CURRENT_RAIDS, SWEDISH), "Aktuella raids");

        i18nMessages.put(new I18nLookup(RAID_BETWEEN, Locale.ENGLISH), "%1-%2");
        i18nMessages.put(new I18nLookup(RAID_BETWEEN, SWEDISH), "%1-%2");

        i18nMessages.put(new I18nLookup(LIST_NO_RAIDS, Locale.ENGLISH), "There are currently no active raids. " +
                "To register a raid, use the following command:\n!raid new {pokemon} {ends at (HH:mm)} {gym}\n" +
                "Example: !raid new Entei 09:45 Solna Platform");
        i18nMessages.put(new I18nLookup(LIST_NO_RAIDS, SWEDISH),
                "Det finns just nu inga registrerade raids. " +
                        "För att registrera en raid, skriv:\n" +
                        "!raid new {pokemon} {sluttid (HH:mm)} {gym}\n" +
                        "Exempel: !raid new Entei 09:45 Solna Platform");

        i18nMessages.put(new I18nLookup(LIST_HELP, Locale.ENGLISH),
                "Check current raids - !raid list [optional: Pokemon]");
        i18nMessages.put(new I18nLookup(LIST_HELP, SWEDISH), "Visa aktuella raids - " +
                "!raid list [Pokemon (frivilligt att ange)]");


        i18nMessages.put(new I18nLookup(IF_CORRECT_MOVESET, Locale.ENGLISH), "(if correct moveset)");
        i18nMessages.put(new I18nLookup(IF_CORRECT_MOVESET, SWEDISH), "(om bra \"moves\")");

        i18nMessages.put(new I18nLookup(OTHER_COUNTERS, Locale.ENGLISH), "Other counters: ");
        i18nMessages.put(new I18nLookup(OTHER_COUNTERS, SWEDISH), "Andra bra val: ");

        i18nMessages.put(new I18nLookup(BEST_COUNTERS, Locale.ENGLISH), "Best counter: ");
        i18nMessages.put(new I18nLookup(BEST_COUNTERS, SWEDISH), "Bästa valet: ");

        i18nMessages.put(new I18nLookup(RESISTANT, Locale.ENGLISH), "Avoid using: ");
        i18nMessages.put(new I18nLookup(RESISTANT, SWEDISH), "Undvik: ");

        i18nMessages.put(new I18nLookup(WEAKNESSES, Locale.ENGLISH), "Use: ");
        i18nMessages.put(new I18nLookup(WEAKNESSES, SWEDISH), "Använd: ");

        i18nMessages.put(new I18nLookup(VS_HELP, Locale.ENGLISH),
                "List information about a pokemon, it's types, weaknesses etc. - !raid vs [Pokemon]");
        i18nMessages.put(new I18nLookup(VS_HELP, SWEDISH),
                "Se information om en pokemon, dess typ, svagheter etc. - !raid vs [Pokemon]");

        i18nMessages.put(new I18nLookup(RAID_TOSTRING, Locale.ENGLISH), "Raid for %1 at gym %2, from %3 to %4");
        i18nMessages.put(new I18nLookup(RAID_TOSTRING, SWEDISH), "%1 vid %2, från %3 till %4");

        i18nMessages.put(new I18nLookup(NEW_RAID_CREATED, Locale.ENGLISH), "Raid created: %1");
        i18nMessages.put(new I18nLookup(NEW_RAID_CREATED, SWEDISH), "Raid skapad: %1");

        i18nMessages.put(new I18nLookup(NEW_RAID_HELP, Locale.ENGLISH),
                "Create new raid - !raid new [Name of Pokemon] [Ends at (HH:MM)] [Gym name]");
        i18nMessages.put(new I18nLookup(NEW_RAID_HELP, SWEDISH),
                "Skapa ny raid - !raid new [Pokemon] [Slutar klockan (HH:MM)] [Gym]");

        i18nMessages.put(new I18nLookup(NEW_EX_RAID_HELP, Locale.ENGLISH),
                "Create new EX raid - !raid ex [Name of Pokemon] [Ends at (yyyy-mm-dd HH:MM)] [Gym name]");
        i18nMessages.put(new I18nLookup(NEW_EX_RAID_HELP, SWEDISH),
                "Skapa ny EX raid - !raid ex [Pokemon] [Slutar (yyyy-mm-dd HH:MM)] [Gym]");

        i18nMessages.put(new I18nLookup(AT_YOUR_SERVICE, Locale.ENGLISH), "PokeRaidBot reporting for duty!");
        i18nMessages.put(new I18nLookup(AT_YOUR_SERVICE, SWEDISH), "PokeRaidBot till er tjänst!");

        i18nMessages.put(new I18nLookup(USAGE_HELP, Locale.ENGLISH), "Shows usage of bot.");
        i18nMessages.put(new I18nLookup(USAGE_HELP, SWEDISH), "Visar hur man använder botten.");

        i18nMessages.put(new I18nLookup(GENERIC_USER_ERROR, Locale.ENGLISH), "%1: %2");
        i18nMessages.put(new I18nLookup(GENERIC_USER_ERROR, SWEDISH), "%1: %2");

        i18nMessages.put(new I18nLookup(USAGE, Locale.ENGLISH), featuresString_EN);
        i18nMessages.put(new I18nLookup(USAGE, SWEDISH), featuresString_SV);

        i18nMessages.put(new I18nLookup(GYM_NOT_FOUND, Locale.ENGLISH), "Could not find Gym with name \"%1\" in region %2");
        i18nMessages.put(new I18nLookup(GYM_NOT_FOUND, SWEDISH), "Kunde inte hitta gym med namn \"%1\" i regionen %2");

        i18nMessages.put(new I18nLookup(RAID_EXISTS, Locale.ENGLISH),
                "Sorry, %1, a raid at gym %2 already exists (for %3). Sign up for it!");
        i18nMessages.put(new I18nLookup(RAID_EXISTS, SWEDISH),
                "Tyvärr, %1, en raid vid gym %2 finns redan (för %3). Anmäl dig till den?");

        i18nMessages.put(new I18nLookup(HELP_MANUAL_HELP_TEXT, Locale.ENGLISH),
                " Help manual for different topics: !raid man {topic} {optional:chan/dm - " +
                        "used when an admin wants to show a user the syntax of commands}\n" +
                        "Available topics: raid, signup, map, install, change, tracking, group, ALL.\n" +
                        "**Example (to get help about raid commands):** !raid man raid"
        );
        i18nMessages.put(new I18nLookup(HELP_MANUAL_HELP_TEXT, SWEDISH),
                " Hjälpmanual för olika ämnen: !raid man {ämne} {frivilligt:chan/dm - " +
                        "om man t.ex. vill visa hjälpen i en textkanal för en användare}\n" +
                        "Möjliga ämnen: raid, signup, map, install, change, tracking, group, ALL.\n" +
                        "**Exempel (för att få hjälp angående raidkommandon):** !raid man raid"
        );
        i18nMessages.put(new I18nLookup(MANUAL_CONFIG, Locale.ENGLISH),
                "**Note: This command must be executed in a server text channel, not in DM!**\n\n" +
                        "**To find out your configuration:**\n!raid config show\n\n" +
                        "**To change language:**\n!raid config *[param=value]*\n" +
                        "*Example setting Swedish locale:* !raid config locale=sv\n" +
                "*Example setting nickname:* !raid config nick=HelloWorld"
        );
        i18nMessages.put(new I18nLookup(MANUAL_CONFIG, SWEDISH),
                "**OBS: Detta kommando måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**För att få reda på din konfiguration:**\n!raid config show\n\n" +
                        "**För att ändra språk:**\n!raid config *[param=value]*\n" +
                        "*Exempel (set English as language):* !raid config locale=en\n" +
                "*Exempel att sätta smeknamn:* !raid config nick=HelloWorld"
        );
        i18nMessages.put(new I18nLookup(MANUAL_RAID, Locale.ENGLISH),
                "**Note: All of these commands must be executed in a server text channel, not in DM!**\n\n" +
                        "**To register a new raid:**\n!raid new *[Pokemon]* *[Ends at (HH:MM)]* *[Gym name]*\n" +
                        "*Example:* !raid new entei 09:25 Solna Platform\n\n" +
                        "**To register a new EX raid:**\n!raid ex *[Pokemon]* *[Ends at (yyyy-mm-dd HH:MM)]* *[Gym name]*\n" +
                        "*Example:* !raid ex deoxys 2017-10-10 09:25 Solna Platform\n\n" +
                        "**To register a new raid via start time:**\n!raid start *[Pokemon]* " +
                        "*[Starts at (HH:MM)]* *[Gym name]*\n" +
                        "*Example:* !raid start entei 08:40 Solna Platform\n\n" +
                        "**To report a egg being hatched at some point:**\n!raid start *[Egg1-5 depending on raid tier]* " +
                        "*[Starts at (HH:MM)]* *[Gym name]*\n" +
                        "*Example:* !raid start Egg5 08:40 Solna Platform\n\n" +
                        "**To report a reported egg having hatched:**\n!raid hatch *[Pokemon]* " +
                        "*[Gym name]*\n" +
                        "*Example:* !raid hatch entei Solna Platform\n\n" +
                        "**Check status for a raid in a gym:**\n!raid status *[Gym name]*\n" +
                        "*Example:* !raid status Solna Platform\n\n" +
                        "**Get a list of all active raids:**\n!raid list\n" +
                        "*Examples:* !raid list Entei - list all raids for Entei.\n" +
                        "!raid list - list all active raids.\n\n" +
                        "**Info about a raid boss:**\n!raid vs *[Pokemon]*\n" +
                        "*Example:* !raid vs Entei"
        );
        i18nMessages.put(new I18nLookup(MANUAL_RAID, SWEDISH),
                "**OBS: Alla dessa kommandon måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**För att registrera en raid:**\n!raid new *[Pokemon]* " +
                        "*[Slutar klockan (HH:MM)]* *[Gym-namn]*\n" +
                        "*Exempel:* !raid new entei 09:25 Solna Platform\n\n" +
                        "**För att registrera en EX raid:**\n!raid ex *[Pokemon]* " +
                        "*[Slutar (yyyy-mm-dd HH:MM)]* *[Gym-namn]*\n" +
                        "*Exempel:* !raid ex deoxys 2017-10-10 09:25 Solna Platform\n\n" +
                        "**För att registrera en raid via starttid:**\n!raid start *[Pokemon]* " +
                        "*[Startar klockan (HH:MM)]* *[Gym-namn]*\n" +
                        "*Exempel:* !raid start entei 08:40 Solna Platform\n\n" +
                        "**För att rapportera att ett raidägg kommer kläckas:**\n!raid start " +
                        "*[Egg1-5 beroende på nivå]* " +
                        "*[Startar klockan (HH:MM)]* *[Gym-namn]*\n" +
                        "*Exempel:* !raid start Egg5 08:40 Solna Platform\n\n" +
                        "**För att rapportera att ett rapporterat ägg kläckts:**\n!raid hatch *[Pokemon]* " +
                        "*[Gym-namn]*\n" +
                        "*Exempel:* !raid hatch entei Solna Platform\n\n" +
                        "**Kolla status för en raid:**\n!raid status *[Gym-namn]*\n" +
                        "*Exempel:* !raid status Solna Platform\n\n" +
                        "**Visa alla registrerade raider:**\n!raid list\n" +
                        "*Exempel:* !raid list Entei - visa alla aktuella raider med Entei som boss.\n" +
                        "!raid list - lista alla aktuella raider oavsett boss.\n\n" +
                        "**Information om en raidboss:**\n!raid vs *[Pokemon]*\n" +
                        "*Exempel:* !raid vs Entei"
        );

        i18nMessages.put(new I18nLookup(MANUAL_CHANGE, Locale.ENGLISH),
                "**Note: All of these commands must be executed in a server text channel, not in DM!**\n\n" +
                        "**Change endtime for a raid:** !raid change when *[New end of raid (HH:MM)]* *[Pokestop name]* " +
                        "(Only raid creator or server admins may do this)\n" +
                        "*Example:* !raid change when 09:45 Solna Platform\n\n" +
                        "**Change start time for a raid group:** !raid change group *[New time (HH:MM)]* *[Pokestop name]*" +
                        " (Only raid creator or server admin)\n" +
                        "*Example:* !raid change group 09:35 Solna Platform*\n\n" +
                        "**Remove raid group:** !raid change group remove *[Time (HH:MM)]* *[Pokestop name]*" +
                        " (Only group creator - if no signups - or server admin)\n" +
                        "*Example:* !raid change group remove 09:35 Solna Platform*\n" +
                        "You can also remove the group message to remove the raid group.\n\n" +
                        "**Change raid boss:** !raid change pokemon *[Pokemon]* *[Pokestop name]* " +
                        "(Only raid creator or server admin)\n" +
                        "*Example:* !raid change pokemon Suicune Solna Platform\n\n" +
                        "**Delete a raid:** !raid change remove *[Pokestop name]* (Only server admin)\n" +
                        "*Example:* !raid change remove Solna Platform"
        );
        i18nMessages.put(new I18nLookup(MANUAL_CHANGE, SWEDISH),
                "**OBS: Alla dessa kommandon måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**Ändra en raids sluttid:** !raid change when *[Ny sluttid (HH:MM)]* *[Gym-namn]* " +
                        "(Endast raidskapare eller admin får göra detta)\n" +
                        "*Exempel:* !raid change when 09:45 Solna Platform\n\n" +
                        "**Ändra tid för en raidgrupp:** !raid change group *[Ny tid (HH:MM)]* *[Gym-namn]*" +
                        " (Endast raidskapare eller admin)\n" +
                        "*Exempel:* !raid change group 09:35 Solna Platform*\n\n" +
                        "**Ta bort raidgrupp:** !raid change group remove *[Time (HH:MM)]* *[Pokestop name]*" +
                        " (Bara gruppskapare - om det inte finns anmälningar - eller admin)\n" +
                        "*Exempel:* !raid change group remove 09:35 Solna Platform*\n" +
                        "*Man kan också ta bort gruppmeddelandet så rensas gruppen automatiskt.\n\n" +
                        "**Ändra en raids boss:** !raid change pokemon *[Pokemon]* *[Pokestop name]* " +
                        "(Endast raidskapare eller admin)\n" +
                        "*Exempel:* !raid change pokemon Suicune Solna Platform\n\n" +
                        "**Ta bort en raid:** !raid change remove *[Pokestop name]* (Endast admin)\n" +
                        "*Exempel:* !raid change remove Solna Platform"
        );

        i18nMessages.put(new I18nLookup(MANUAL_GROUPS, Locale.ENGLISH),
                "**Note:This command must be executed in a server text channel, not in DM!**\n\n" +
                        "**Create a group to run a raid at a certain time:** !raid group {time (HH:MM)} {gym name}\n" +
                        "*Example:* !raid group 09:45 Solna Platform\n\n" +
                        "A user can sign up themselves and their group friends via emotes below the raid group message.\n\n" +
                        "1-6 buttons signs up the corresponding amount of people. " +
                        "You can combine numbers to get the correct total. " +
                        "Press the same button(s) again to remove the signups.\n\n" +
                        "The message will be automatically updated with new signups. When the raid group start time" +
                        " is expired, the message will be removed, along with the associated signups.\n\n" +
                        "Since this function is pretty new, feedback on how to improve it is welcome."
        );
        i18nMessages.put(new I18nLookup(MANUAL_GROUPS, SWEDISH),
                "**OBS: Kommandot måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**Skapa en grupp för att köra raid en viss tid:** !raid group {time (HH:MM)} {gym namn}\n" +
                        "*Exempel:* !raid group 09:45 Solna Platform\n\n" +
                        "Man anmäler sig till en raid via emotes som dyker upp under svaret på kommandot ovan.\n\n" +
                        "1-6 anmäler det antal som står på knappen. " +
                        "Tryck samma knapp igen för att ta bort samma antal.\n\n" +
                        "Meddelandet kommer uppdateras var 15:e sekund med alla som anmäler sig. När tiden gått " +
                        "ut för gruppen, kommer meddelandet tas bort, tillsammans med alla relaterade anmälningar.\n\n" +
                        "Eftersom denna funktion är tämligen ny, uppskattas feedback på hur man kan göra den bättre."
        );

        i18nMessages.put(new I18nLookup(MANUAL_INSTALL, Locale.ENGLISH),
                "**Install configuration for this server:** !raid install - starts install process\n" +
                        "!raid install server=[server name];region=[region dataset reference];" +
                        "replyInDm=[true or false];locale=[2 char language code]\n" +
                        "**Example:** !raid install server=My test server;region=stockholm;replyInDm=false;locale=sv"
        );
        i18nMessages.put(new I18nLookup(MANUAL_INSTALL, SWEDISH),
                "**Installera konfiguration för denna server:** !raid install - startar processen\n" +
                        "!raid install server=[servernamn];region=[region, datasetsreferens];" +
                        "replyInDm=[true eller false];locale=[2 teckens språkkod, t.ex. sv]\n" +
                        "**Exempel:** !raid install server=My test server;region=stockholm;replyInDm=false;locale=sv"
        );

        i18nMessages.put(new I18nLookup(MANUAL_MAP, Locale.ENGLISH),
                "**Note: This command must be executed in a server text channel, not in DM!**\n\n" +
                        "**Get map for a certain gym:**\n!raid map *[Gym name]*\n" +
                        "*Example:* !raid map Solna Platform\n\n" +
                        "Note: You can click the name of the gym to go to it in Google Maps. There, you " +
                        "can get directions, estimated time of arrival etc."
        );
        i18nMessages.put(new I18nLookup(MANUAL_MAP, SWEDISH),
                "**OBS: Detta kommando måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**Hämta karta för gym:**\n!raid map *[Gym-namn]*\n" +
                        "*Exempel:* !raid map Solna Platform\n\n" +
                        "OBS: Du kan klicka på gymnamnet för att gå till det i Google Maps. Där kan du sedan få t.ex. " +
                        "vägbeskrivning, beräknad ankomsttid och så vidare."
        );

        i18nMessages.put(new I18nLookup(MANUAL_SIGNUP, Locale.ENGLISH),
                "**Note: All of these commands must be executed in a server text channel, not in DM!**\n\n" +
                        "**Sign up for a raid:**\n!raid add *[number of people] [ETA (HH:MM)] [Gym name]*\n" +
                        "*Example:* !raid add 3 09:15 Solna Platform\n\n" +
                        "**You can also use an easier way:** \\+{number of people} {ETA (HH:MM)} {Gym name}\n" +
                        "*Example:* +3 09:15 Solna Platform\n\n" +
                        "**Unsign raid:**\n!raid remove *[Gym name]*\n" +
                        "*Example:* !raid remove Solna Platform\n\n" +
                        "**You can also use an easier way:** \\-{number of people} {Gym name}\n" +
                        "*Example:* -2 Solna Platform"
        );
        i18nMessages.put(new I18nLookup(MANUAL_SIGNUP, SWEDISH),
                "**OBS: Alla dessa kommandon måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**Säg att du kommer till en viss raid:**\n!raid add *[antal som kommer] [ETA (HH:MM)] [Gym-namn]*\n" +
                        "*Exempel:* !raid add 3 09:15 Solna Platform\n" +
                        "**Man kan också använda:** \\+{antal} {ETA (HH:MM)} {Gym-namn}\n" +
                        "*Exempel:* +3 09:15 Solna Platform\n\n" +
                        "**Ta bort din signup för en raid:**\n!raid remove *[Gym-namn]*\n" +
                        "*Exempel:* !raid remove Solna Platform\n\n" +
                        "**Man kan också använda:** \\-{antal} {Gym-namn}\n" +
                        "*Exempel:* -2 Solna Platform"
        );

        i18nMessages.put(new I18nLookup(MANUAL_TRACKING, Locale.ENGLISH),
                "**Note: All of these commands must be executed in a server text channel, not in DM!**\n\n" +
                        "**Track new raids for raid boss:**\n" +
                        "!raid track *[Pokemon]*\n" +
                        "*Example:* !raid track Entei\n\n" +
                        "**Untrack raids for raid boss:**\n!raid untrack *[Pokemon]*\n" +
                        "*Example - remove your tracking of Entei:* !raid untrack Entei\n" +
                        "*Example - remove all your tracking:* !raid untrack"
        );
        i18nMessages.put(new I18nLookup(MANUAL_TRACKING, SWEDISH),
                "**OBS: Alla dessa kommandon måste köras i en servers textkanal, inte i DM!**\n\n" +
                        "**Övervakning av nya raids för pokemon:" +
                        "**\n!raid track *[Pokemon]*\n" +
                        "*Exempel:* !raid track Entei\n\n" +
                        "**Ta bort övervakning av nya raids för pokemon:**\n!raid untrack *[Pokemon]*\n" +
                        "*Exempel - ta bort din övervakning för Entei:* !raid untrack Entei\n" +
                        "*Exempel - ta bort alla dina övervakningar:* !raid untrack"
        );
    }

    public Locale getLocaleForUser(User user) {
        if (userConfigRepository == null || user == null || user.getId() == null) {
            return DEFAULT;
        }

        final UserConfig userConfig = userConfigRepository.findOne(user.getId());
        if (userConfig == null) {
            return DEFAULT;
        } else {
            final Locale userLocale = userConfig.getLocale();
            if (userLocale == null) {
                return DEFAULT;
            } else {
                return userLocale;
            }
        }
    }

    public Locale getLocaleForUser(String username) {
        return DEFAULT;
    }

    public void storeMessage(String messageKey, Locale locale, String message) {
        i18nMessages.put(new I18nLookup(messageKey.toUpperCase(), locale), message);
    }

    public String getMessageFor(String messageKey, Locale locale, String ... parameters) {
        String messageWithParameters = getMessageTextToInjectParametersIn(messageKey, locale);
        int i = 1;
        for (String param : parameters) {
            messageWithParameters = messageWithParameters.replaceAll("[%][" + i + "]", param);
            i++;
        }
        return messageWithParameters;
    }

    private String getMessageTextToInjectParametersIn(String messageKey, Locale locale) {
        Locale actualLocale = locale;
        if (locale == null) {
            actualLocale = DEFAULT;
        }
        String message = i18nMessages.get(new I18nLookup(messageKey.toUpperCase(), actualLocale));
        if (message == null || message.length() < 1) {
            message = i18nMessages.get(new I18nLookup(messageKey.toUpperCase(), DEFAULT));
        }
        if (message == null || message.length() < 1) {
            throw new RuntimeException("Could not find text for message key " + messageKey +
                    " - an admin needs to add it to the LocaleService!");
        }
        return message;
    }

    public static boolean isSupportedLocale(Locale locale) {
        return Arrays.asList(SUPPORTED_LOCALES).contains(locale);
    }

    public static String asString(TimeUnit timeUnit, Locale locale) {
        switch (timeUnit) {
            case SECONDS:
                return (locale != null && locale.getLanguage().equals(SWEDISH.getLanguage()) ? "sekund" : "second");
            case MINUTES:
                return (locale != null && locale.getLanguage().equals(SWEDISH.getLanguage()) ? "minut" : "minute");
            default:
                return timeUnit.name().toLowerCase();
        }
    }

    private class I18nLookup {
        private String messageKey;
        private Locale locale;

        I18nLookup(String messageKey, Locale locale) {
            this.messageKey = messageKey;
            this.locale = locale;
        }

        public String getMessageKey() {
            return messageKey;
        }

        public Locale getLocale() {
            return locale;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof I18nLookup)) return false;

            I18nLookup that = (I18nLookup) o;

            if (messageKey != null ? !messageKey.equals(that.messageKey) : that.messageKey != null) return false;
            return locale != null ? locale.equals(that.locale) : that.locale == null;
        }

        @Override
        public int hashCode() {
            int result = messageKey != null ? messageKey.hashCode() : 0;
            result = 31 * result + (locale != null ? locale.hashCode() : 0);
            return result;
        }
    }

    public static String featuresString_EN =
            "Pokeraidbot - a Discord bot to help Pokémon Go raiders. Raid, map, pokemon info functions.\n\n" +
                    "Getting started guide: " +
                    "https://github.com/magnusmickelsson/pokeraidbot/blob/master/GETTING_STARTED_USER_en.md\n\n" +
                    "**Get detailed help about how the bot works:**\n!raid man\n\n" +
                    "**To see one of its features, type the following:** " +
                    "!raid map {name of a raid gym in your vicinity}\n" +
                    "*Example:* !raid map Solna Platform\n\n" +
                    "https://github.com/magnusmickelsson/pokeraidbot to report errors, request features, " +
                    "see screenshots of usage etc.\n\n" +
            "**How do I support development of this bot?**\n!raid donate";
    public static String featuresString_SV =
            "Pokeraidbot - en Discord-bot för att hjälpa Pokémon Go raiders, med t.ex. kartor till gym, raidplanering" +
                    " och information om pokemons.\n\n" +
                    "Kom igång guide: " +
                    "https://github.com/magnusmickelsson/pokeraidbot/blob/master/GETTING_STARTED_USER_sv.md\n\n" +
                    "**Få detaljerad hjälp om hur botten fungerar:**\n!raid man {frivilligt: ämne}\n" +
                    "*Exempel:* !raid man - berättar om hur man använder raid man och vilka hjälpämnen som finns\n\n" +
                    "**För ett exempel på vad botten kan göra, skriv:** !raid map {namn på ett gym i området}\n" +
                    "*Exempel:* !raid map Solna Platform\n\n" +
                    "https://github.com/magnusmickelsson/pokeraidbot för att rapportera fel, önska funktioner, " +
                    "se screenshots av användning etc.\n\n" +
            "**Hur kan jag stödja utveckling av botten?**\n!raid donate\n\n" +
            "**If you want this information in english:**\n!raid usage en";
} public class Emotes {
    public static final String SIGN_UP_NOW_EMOTE = "\uD83D\uDC4B";
    public static final String UNSIGN_EMOTE = "\uD83D\uDEB7";
    public static final String SIGNUP_GROUP_EMOTE = "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67";
    public static final String OK = "✅";
    public static final String ERROR = "❌";
    public static final String ONE = "1⃣";
    public static final String TWO = "2⃣";
    public static final String THREE = "3⃣";
    public static final String FOUR = "4⃣";
    public static final String FIVE = "5⃣";
    public static final String SIX = "6⃣";
    public static final String SAD = "\uD83D\uDC94";
    public static final String HAPPY = "\uD83D\uDE03";
    public static final String GROUP = "\uD83D\uDC65";
    public static final String STAR = "⭐";
} public class GymRepository {
    private static final Logger LOGGER = LoggerFactory.getLogger(GymRepository.class);

    private Map<String, Set<Gym>> gymsPerRegion = new ConcurrentHashMap<>();
    private Map<String, Set<String>> exGymsPerRegion = new ConcurrentHashMap<>();
    private final ServerConfigRepository serverConfigRepository;
    private final LocaleService localeService;

    public GymRepository(ServerConfigRepository serverConfigRepository, LocaleService localeService) {
        this.serverConfigRepository = serverConfigRepository;
        this.localeService = localeService;
    }

    // Only used for testing!
    public GymRepository(Map<String, Set<Gym>> gyms, LocaleService localeService) {
        this.localeService = localeService;
        this.serverConfigRepository = null;
        for (String region : gyms.keySet()) {
            Set<Gym> gymsForRegion = gyms.get(region);
            this.gymsPerRegion.put(region, gymsForRegion);
        }
    }

    public void reloadGymData() {
        if (serverConfigRepository != null) {
            Map<String, Config> configMap = serverConfigRepository.getAllConfig();
            Map<String, Set<Gym>> gymsPerRegion = new HashMap<>();
            LOGGER.info("Config has following servers: " + configMap.keySet());
            for (String server : configMap.keySet()) {
                final Config config = serverConfigRepository.getConfigForServer(server);
                final String region = config.getRegion();
                final Set<Gym> existingGyms = gymsPerRegion.get(region);
                if (existingGyms == null) {
                    try {
                        final Set<Gym> gymsInRegion = new CSVGymDataReader("/gyms_" + region + ".csv").readAll();
                        gymsPerRegion.put(region, gymsInRegion);
                        LOGGER.info("Loaded " + gymsInRegion.size() + " gyms for region " + region + ".");
                    } catch (Throwable t) {
                        LOGGER.warn("Could not load data for region " + region + ", skipping.");
                    }
                }
            }

            for (String region : gymsPerRegion.keySet()) {
                Set<Gym> gymsForRegion = gymsPerRegion.get(region);
                this.gymsPerRegion.put(region, gymsForRegion);
            }
        }
    }

    public Gym search(User user, String query, String region) {
        if (region == null || StringUtils.isEmpty(query)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.GYM_SEARCH,
                    LocaleService.DEFAULT));
        }

        final Set<Gym> gyms = getAllGymsForRegion(region);

        final Locale localeForUser = localeService.getLocaleForUser(user);
        final Optional<Gym> gym = get(query, region);
        if (gym.isPresent()) {
            return gym.get();
        } else {
            //70 seems like a reasonable cutoff here...
            List<ExtractedResult> candidates = extractTop(query, gyms.stream().map(
                    s -> s.getName()).collect(Collectors.toList()), 6, 70);
            if (candidates.size() == 1) {
                return findByName(candidates.iterator().next().getString(), region);
            } else if (candidates.size() < 1) {
                throw new GymNotFoundException(query, localeService, LocaleService.SWEDISH, region);
            } else {
                List<Gym> matchingPartial = getMatchingPartial(query, region, candidates);
                if (matchingPartial.size() == 1) {
                    return matchingPartial.get(0);
                }
                if (candidates.size() <= 5) {
                    String possibleMatches = candidates.stream().map(s -> findByName(s.getString(), region)
                            .getName()).collect(Collectors.joining(", "));
                    throw new UserMessedUpException(user,
                            localeService.getMessageFor(LocaleService.GYM_SEARCH_OPTIONS, localeForUser,
                                    possibleMatches));
                } else {
                    throw new UserMessedUpException(user,
                            localeService.getMessageFor(LocaleService.GYM_SEARCH_MANY_RESULTS, localeForUser));
                }
            }
        }
    }

    public Gym findByName(String name, String region) {
        final Optional<Gym> gym = get(name, region);
        if (!gym.isPresent()) {
            throw new GymNotFoundException(name, localeService, LocaleService.SWEDISH, region);
        }
        return gym.get();
    }

    // Temporary method to work around problem with gym data updates from the map site - will be removed when we move
    // gym data to database and do an administrative UI to maintain them
    public void addTemporary(User user, Gym gym, String region) {
        if (get(gym.getName(), region).isPresent()) {
            throw new UserMessedUpException(user, localeService.getMessageFor(
                    LocaleService.COULD_NOT_ADD_GYM,
                    localeService.getLocaleForUser(user)));
        }
        final Set<Gym> gymsForRegion = new HashSet<>(gymsPerRegion.get(region));
        gymsForRegion.add(gym);
        gymsPerRegion.put(region, gymsForRegion);
    }

    public Gym findById(String id, String region) {
        for (Gym gym : getAllGymsForRegion(region)) {
            if (gym.getId().equals(id))
                return gym;
        }
        throw new GymNotFoundException("[No entry]", localeService, LocaleService.SWEDISH, region);
    }

    public Set<Gym> getAllGymsForRegion(String region) {
        final Set<Gym> gyms = gymsPerRegion.get(region);
        if (gyms == null || gyms.size() < 1) {
            throw new RuntimeException(localeService.getMessageFor(LocaleService.GYM_CONFIG_ERROR, LocaleService.DEFAULT));
        }
        return gyms;
    }

    private Optional<Gym> get(String name, String region) {
        return getAllGymsForRegion(region).stream().filter(s -> s.getName().equalsIgnoreCase(name)).findFirst();
    }

    private List<Gym> getMatchingPartial(String query, String region, List<ExtractedResult> candidates) {
        String cleanQuery = query.trim().replaceAll(" +", " ");
        List<Gym> mathingGyms = new ArrayList<>();
        for (ExtractedResult result : candidates) {
            if (containsIgnoreCase(result.getString(), cleanQuery)) {
                mathingGyms.add(findByName(result.getString(), region));
            }
        }
        return mathingGyms;
    }

    public Map<String, Set<Gym>> getAllGymData() {
        return Collections.unmodifiableMap(gymsPerRegion);
    }

    public Set<String> getExGyms(String region) {
        Set<String> exGyms = exGymsPerRegion.get(region);
        if (exGyms == null) {
            exGyms = loadExGyms(region);
            exGymsPerRegion.put(region, exGyms);
        }
        return exGyms;
    }

    private Set<String> loadExGyms(String region) {
        Set<String> exGymNamesForRegion;
        final String fileName = "/gyms_" + region.toLowerCase() + ".csv.ex.txt";
        final InputStream inputStreamEx = PotentialExRaidListCommand.class.getResourceAsStream(fileName);
        exGymNamesForRegion = CSVGymDataReader.readExGymListIfExists(inputStreamEx, fileName);
        return exGymNamesForRegion;
    }
} public class PokemonRepository {
    private static final Logger LOGGER = LoggerFactory.getLogger(PokemonRepository.class);

    private final LocaleService localeService;
    public static final String EGG_1 = "Egg1";
    public static final String EGG_2 = "Egg2";
    public static final String EGG_3 = "Egg3";
    public static final String EGG_4 = "Egg4";
    public static final String EGG_5 = "Egg5";
    public static final String EGG_6 = "Egg6";
    private Map<String, Pokemon> pokemons = new LinkedHashMap<>();

    public PokemonRepository(String resourceName, LocaleService localeService) {
        this.localeService = localeService;
        try {
            CSVPokemonDataReader csvPokemonDataReader = new CSVPokemonDataReader(resourceName);
            final Set<Pokemon> pokemonsRead = csvPokemonDataReader.readAll();
            for (Pokemon p : pokemonsRead) {
                pokemons.put(p.getName().toUpperCase(), p);
            }
            // So we can handle eggs before they're hatched
            this.pokemons.put("EGG1", new Pokemon(99999, EGG_1, "Tier 1 egg", PokemonTypes.NONE,
                    "", new HashSet<>(),
                    new HashSet<>()));
            this.pokemons.put("EGG2", new Pokemon(99998, EGG_2, "Tier 2 egg", PokemonTypes.NONE,
                    "", new HashSet<>(),
                    new HashSet<>()));
            this.pokemons.put("EGG3", new Pokemon(99997, EGG_3, "Tier 3 egg", PokemonTypes.NONE,
                    "", new HashSet<>(),
                    new HashSet<>()));
            this.pokemons.put("EGG4", new Pokemon(99996, EGG_4, "Tier 4 egg", PokemonTypes.NONE,
                    "", new HashSet<>(),
                    new HashSet<>()));
            this.pokemons.put("EGG5", new Pokemon(99995, EGG_5, "Tier 5 egg", PokemonTypes.NONE,
                    "", new HashSet<>(),
                    new HashSet<>()));
            this.pokemons.put("EGG6", new Pokemon(99994, EGG_6, "MEGA raid", PokemonTypes.NONE,
                    "", new HashSet<>(),
                    new HashSet<>()));
        } catch (Throwable e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    public Pokemon search(String name, User user) {
        final Pokemon pokemon = fuzzySearch(name);
        if (pokemon == null) {
            final Locale locale = user == null ? LocaleService.DEFAULT : localeService.getLocaleForUser(user);
            throw new RuntimeException(localeService.getMessageFor(LocaleService.NO_POKEMON, locale, name));
        }
        return pokemon;
    }

    public Pokemon getByName(String name) {
        if (name == null) {
            return null;
        }
        final String nameToGet = name.trim().toUpperCase();
        return pokemons.get(nameToGet);
    }

    // This method is a getter with fuzzy search that doesn't throw an exception if it doesn't find a pokemon, just returns null
    private Pokemon fuzzySearch(String name) {
        final Collection<Pokemon> allPokemons = pokemons.values();

        final String nameToSearchFor = name.trim().toUpperCase();
        final Optional<Pokemon> pokemon = Optional.ofNullable(pokemons.get(nameToSearchFor));
        if (pokemon.isPresent()) {
            return pokemon.get();
        } else {
            List<ExtractedResult> candidates = FuzzySearch.extractTop(nameToSearchFor,
                    allPokemons.stream().map(p -> p.getName().toUpperCase()).collect(Collectors.toList()), 5, 50);
            if (candidates.size() == 1) {
                return pokemons.get(candidates.iterator().next().getString());
            } else if (candidates.size() < 1) {
                return null;
            } else {
                int score = 0;
                String highestScoreResultName = null;
                for (ExtractedResult result : candidates) {
                    if (result.getScore() > score) {
                        score = result.getScore();
                        highestScoreResultName = result.getString();
                    }
                }
                if (highestScoreResultName != null) {
                    return pokemons.get(highestScoreResultName);
                } else {
                    return null;
                }
            }
        }
    }

    public Set<Pokemon> getAll() {
        return Collections.unmodifiableSet(new HashSet<>(pokemons.values()));
    }

    public Pokemon getByNumber(Integer pokemonNumber) {
        for (Pokemon p : getAll()) {
            if (Objects.equals(p.getNumber(), pokemonNumber)) {
                return p;
            }
        }
        throw new RuntimeException(localeService.getMessageFor(LocaleService.NO_POKEMON, LocaleService.DEFAULT, "" +
                pokemonNumber));
    }
} public class PokemonRaidStrategyService {
    private static final Logger LOGGER = LoggerFactory.getLogger(PokemonRaidStrategyService.class);

    private Map<String, RaidBossCounters> counters = new HashMap<>();
    private Map<String, PokemonRaidInfo> pokemonRaidInfo = new HashMap<>();
    private static String[] raidBosses = {
            "BAYLEEF",
            "CROCONAW",
            "MAGIKARP",
            "QUILAVA",
            "ELECTABUZZ",
            "EXEGGUTOR",
            "MAGMAR",
            "MUK",
            "WEEZING",
            "ALAKAZAM",
            "ARCANINE",
            "FLAREON",
            "GENGAR",
            "JOLTEON",
            "MACHAMP",
            "VAPOREON",
            "Blastoise".toUpperCase(),
            "Charizard".toUpperCase(),
            "Lapras".toUpperCase(),
            "Rhydon".toUpperCase(), "Snorlax".toUpperCase(), "Tyranitar".toUpperCase(),
            "Venusaur".toUpperCase(), "Entei".toUpperCase(), "Articuno".toUpperCase(), "Moltres".toUpperCase(),
            "Zapdos".toUpperCase(), "Lugia".toUpperCase(),
            "Raikou".toUpperCase(), "Suicune".toUpperCase(),
            "Mewtwo".toUpperCase(),
            // Tier 1
            "Ivysaur".toUpperCase(),
            "Metapod".toUpperCase(),
            "Charmeleon".toUpperCase(),
            "Wartortle".toUpperCase(),
            "egg1".toUpperCase(),
            // Tier 2
            "Magneton".toUpperCase(),
            "Sableye".toUpperCase(),
            "Sandslash".toUpperCase(),
            "Tentacruel".toUpperCase(),
            "Marowak".toUpperCase(), // Note: Alolan is tier 4 and active right now because niantic sucks
            "Cloyster".toUpperCase(),
            "egg2".toUpperCase(),
            // Tier 3
            "Ninetales".toUpperCase(),
            "Scyther".toUpperCase(),
            "Omastar".toUpperCase(),
            "Porygon".toUpperCase(),
            "egg3".toUpperCase(),
            // Tier 4
            "Poliwrath".toUpperCase(),
            "Victreebel".toUpperCase(),
            "Golem".toUpperCase(),
            "Nidoking".toUpperCase(),
            "Nidoqueen".toUpperCase(),
            "egg4".toUpperCase(),
            // Tier 5 /EX
            "egg5".toUpperCase(),
            "Ho-oh".toUpperCase(),
            // ==== Generation 3+ ====
            // Tier 1
            "Wailmer".toUpperCase(),
            "snorunt".toUpperCase(),
            "swablu".toUpperCase(),
            "duskull".toUpperCase(),
            "shuppet".toUpperCase(),
            "bulbasaur".toUpperCase(),
            "charmander".toUpperCase(),
            "squirtle".toUpperCase(),
            "makuhita".toUpperCase(),
            "meditite".toUpperCase(),
            "shinx".toUpperCase(),
            "buizel".toUpperCase(),
            "mareep".toUpperCase(),
            "magnemite".toUpperCase(),
            "minun".toUpperCase(),
            "plusle".toUpperCase(),
            "wingull".toUpperCase(),
            "dratini".toUpperCase(),
            "feebas".toUpperCase(),
            "lotad".toUpperCase(),
            "chikorita".toUpperCase(),
            "sunkern".toUpperCase(),
            "Kricketot".toUpperCase(),
            "Skorupi".toUpperCase(),
            "Caterpie".toUpperCase(),
            "Drifloon".toUpperCase(),
            "Bagon".toUpperCase(),
            "Bronzor".toUpperCase(),
            "Horsea".toUpperCase(),
            "Nidoran".toUpperCase(),
            "Sentret".toUpperCase(),
            "Murkrow".toUpperCase(),
            "snubbull".toUpperCase(),
            "patrat".toUpperCase(),
            "lillipup".toUpperCase(),
            "klink".toUpperCase(),
            "beldum".toUpperCase(),
            "zubat".toUpperCase(),
            "ekans".toUpperCase(),
            "cubone".toUpperCase(),
            "koffing".toUpperCase(),
            "drowzee".toUpperCase(),
            "ralts".toUpperCase(),
            "burmy".toUpperCase(),
            "yanma".toUpperCase(),
            "litwick".toUpperCase(),
            "pidove".toUpperCase(),
            "tepig".toUpperCase(),
            "ducklett".toUpperCase(),
            "espurr".toUpperCase(),
            "trapinch".toUpperCase(),

            // Tier 2
            "Mawile".toUpperCase(),
            "dewgong".toUpperCase(),
            "slowbro".toUpperCase(),
            "Manectric".toUpperCase(),
            "Sneasel".toUpperCase(),
            "Misdreavus".toUpperCase(),
            "lickitung".toUpperCase(),
            "venomoth".toUpperCase(),
            "combusken".toUpperCase(),
            "primeape".toUpperCase(),
            "kirlia".toUpperCase(),
            "roselia".toUpperCase(),
            "lanturn".toUpperCase(),
            "grovyle".toUpperCase(),
            "marshtomp".toUpperCase(),
            "monferno".toUpperCase(),
            "Combee".toUpperCase(),
            "Masquerain".toUpperCase(),
            "pineco".toUpperCase(),
            "houndour".toUpperCase(),
            "gligar".toUpperCase(),
            "feebas".toUpperCase(),
            "yamask".toUpperCase(),
            "anorith".toUpperCase(),
            "lileep".toUpperCase(),
            "kingler".toUpperCase(),
            "magneton".toUpperCase(),
            "grotle".toUpperCase(),
            "grumpig".toUpperCase(),
            "fearow".toUpperCase(),
            "swellow".toUpperCase(),

            // Tier 3
            "azumarill".toUpperCase(),
            "jynx".toUpperCase(),
            "piloswine".toUpperCase(),
            "Aerodactyl".toUpperCase(),
            "Starmie".toUpperCase(),
            "Claydol".toUpperCase(),
            "Granbull".toUpperCase(),
            "Pinsir".toUpperCase(),
            "aerodactyl".toUpperCase(),
            "kabutops".toUpperCase(),
            "onix".toUpperCase(),
            "hitmonlee".toUpperCase(),
            "hitmonchan".toUpperCase(),
            "breloom".toUpperCase(),
            "raichu".toUpperCase(),
            "donphan".toUpperCase(),
            "tangela".toUpperCase(),
            "Sharpedo".toUpperCase(),
            "Skarmory".toUpperCase(),
            "Espeon".toUpperCase(),
            "Umbreon".toUpperCase(),
            "Crawdaunt".toUpperCase(),
            "Lunatone".toUpperCase(),
            "Solrock".toUpperCase(),
            "Shuckle".toUpperCase(),
            "Skuntank".toUpperCase(),
            "Persian".toUpperCase(),
            "pidgeot".toUpperCase(),
            "mantine".toUpperCase(),
            "hippowdon".toUpperCase(),
            "meganium".toUpperCase(),

            // Tier 4
            "feraligatr".toUpperCase(),
            "Absol".toUpperCase(),
            "Salamence".toUpperCase(),
            "Aggron".toUpperCase(),
            "Walrein".toUpperCase(),
            "Houndoom".toUpperCase(),
            "Togetic".toUpperCase(),
            "Metagross".toUpperCase(),
            "Shiftry".toUpperCase(),
            "Ursaring".toUpperCase(),
            "ninjask".toUpperCase(),
            "Regice".toUpperCase(),
            "Regirock".toUpperCase(),
            "Registeel".toUpperCase(),
            "Meowth".toUpperCase(),
            "Excadrill".toUpperCase(),
            "Dragonite".toUpperCase(),
            "Staraptor".toUpperCase(),

            // Tier 5
            "Groudon".toUpperCase(),
            "Kyogre".toUpperCase(),
            "Rayquaza".toUpperCase(),
            "Latios".toUpperCase(),
            "Latias".toUpperCase(),
            "Mew".toUpperCase(),
            "Celebi".toUpperCase(),
            "Giratina".toUpperCase(),
            "palkia".toUpperCase(),
            "dialga".toUpperCase(),
            "cresselia".toUpperCase(),
            "heatran".toUpperCase(),
            "uxie".toUpperCase(),
            "mesprit".toUpperCase(),
            "azelf".toUpperCase(),
            "darkrai".toUpperCase(),
            "cobalion".toUpperCase(),
            "terrakion".toUpperCase(),
            "virizion".toUpperCase(),
            "regigigas".toUpperCase(),
            "tornadus".toUpperCase(),
            "thundurus".toUpperCase(),
            "landorus".toUpperCase(),
            "zacian".toUpperCase(),
            "zamazenta".toUpperCase(),
            "deoxys".toUpperCase()
    };

    public PokemonRaidStrategyService(PokemonRepository pokemonRepository) {
        for (String raidBossName : raidBosses) {
            try {
                final CounterTextFileParser parser = new CounterTextFileParser("/counters", raidBossName, pokemonRepository);
                final Pokemon raidBoss = pokemonRepository.getByName(raidBossName);
                if (raidBoss == null) {
                    LOGGER.error("Could not find raidBoss in pokemon repository: " + raidBossName);
                    System.exit(-1);
                }
                final RaidBossCounters raidBossCounters = new RaidBossCounters(raidBoss, parser.getBestCounters(), parser.getGoodCounters());
                counters.put(raidBossName.toUpperCase(), raidBossCounters);
            } catch (RuntimeException e) {
                // No file for this boss, skip it
            }
        }
        LOGGER.info("Parsed " + counters.size() + " raid boss counters.");

        populateRaidInfoForBoss(pokemonRepository, "BAYLEEF", "740", 1);

        populateRaidInfoForBoss(pokemonRepository, "CROCONAW", "984", 2);

        populateRaidInfoForBoss(pokemonRepository, "MAGIKARP", "157", 1);

        populateRaidInfoForBoss(pokemonRepository, "QUILAVA", "847", 1);

        populateRaidInfoForBoss(pokemonRepository, "ELECTABUZZ", "1333", 2);

        populateRaidInfoForBoss(pokemonRepository, "EXEGGUTOR", "1722", 2);

        populateRaidInfoForBoss(pokemonRepository, "MAGMAR", "1288", 2);

        populateRaidInfoForBoss(pokemonRepository, "MUK", "1575", 2);

        // Galarian version so temporarily changed
        populateRaidInfoForBoss(pokemonRepository, "WEEZING", "1310", 4);

        populateRaidInfoForBoss(pokemonRepository, "ALAKAZAM", "1747", 3);

        populateRaidInfoForBoss(pokemonRepository, "ARCANINE", "1622", 3);

        populateRaidInfoForBoss(pokemonRepository, "FLAREON", "1730", 3);

        populateRaidInfoForBoss(pokemonRepository, "GENGAR", "1496", 3);

        populateRaidInfoForBoss(pokemonRepository, "JOLTEON", "1650", 3);

        populateRaidInfoForBoss(pokemonRepository, "MACHAMP", "1746", 3);

        populateRaidInfoForBoss(pokemonRepository, "VAPOREON", "1779", 3);

        populateRaidInfoForBoss(pokemonRepository, "Blastoise", "1309", 4);

        populateRaidInfoForBoss(pokemonRepository, "Charizard", "1535", 4);

        populateRaidInfoForBoss(pokemonRepository, "Lapras", "1487", 4);

        populateRaidInfoForBoss(pokemonRepository, "Rhydon", "1816", 4);

        populateRaidInfoForBoss(pokemonRepository, "Snorlax", "1917", 4);

        populateRaidInfoForBoss(pokemonRepository, "Tyranitar", "2191", 4);

        populateRaidInfoForBoss(pokemonRepository, "Venusaur", "1467", 4);

        populateRaidInfoForBoss(pokemonRepository, "Entei", "1930", 5);

        populateRaidInfoForBoss(pokemonRepository, "Articuno", "1676", 5);

        populateRaidInfoForBoss(pokemonRepository, "Moltres", "1870", 5);

        populateRaidInfoForBoss(pokemonRepository, "Zapdos", "1902", 5);

        populateRaidInfoForBoss(pokemonRepository, "Lugia", "2056", 5);

        populateRaidInfoForBoss(pokemonRepository, "Raikou", "1913", 5);

        populateRaidInfoForBoss(pokemonRepository, "Suicune", "1613", 5);

        populateRaidInfoForBoss(pokemonRepository, "Mewtwo", "2387", 5);

        // New bosses after Niantic surprise attack :p
        // Tier 1
        populateRaidInfoForBoss(pokemonRepository, "Ivysaur", "886", 1);
        populateRaidInfoForBoss(pokemonRepository, "Metapod", "239", 1);
        populateRaidInfoForBoss(pokemonRepository, "Charmeleon", "847", 1);
        populateRaidInfoForBoss(pokemonRepository, "Wartortle", "756", 1);
        populateRaidInfoForBoss(pokemonRepository, "egg1", "1", 1);
        // Tier 2
        populateRaidInfoForBoss(pokemonRepository, "Magneton", "1278", 2);
        populateRaidInfoForBoss(pokemonRepository, "Sableye", "843", 2);
        populateRaidInfoForBoss(pokemonRepository, "Sandslash", "1356", 2);
        populateRaidInfoForBoss(pokemonRepository, "Tentacruel", "1356", 2);
        populateRaidInfoForBoss(pokemonRepository, "Cloyster", "1414", 2);
        populateRaidInfoForBoss(pokemonRepository, "Kingler", "1616", 2);
        populateRaidInfoForBoss(pokemonRepository, "egg2", "1", 2);
        // Normally tier 2 but alolan raid is tier 4 because niantic sucks in not letting different pokemons have different id's etc.
        populateRaidInfoForBoss(pokemonRepository, "Marowak", "1048", 4);
        // Tier 3
        populateRaidInfoForBoss(pokemonRepository, "Ninetales", "1233", 3);
        populateRaidInfoForBoss(pokemonRepository, "Scyther", "1546", 3);
        populateRaidInfoForBoss(pokemonRepository, "Omastar", "1534", 3);
        populateRaidInfoForBoss(pokemonRepository, "Porygon", "895", 3);
        populateRaidInfoForBoss(pokemonRepository, "egg3", "1", 3);

        // Tier 4
        populateRaidInfoForBoss(pokemonRepository, "Poliwrath", "1395", 4);
        populateRaidInfoForBoss(pokemonRepository, "Victreebel", "1296", 4);
        populateRaidInfoForBoss(pokemonRepository, "Golem", "1685", 4);
        populateRaidInfoForBoss(pokemonRepository, "Nidoking", "1363", 4);
        populateRaidInfoForBoss(pokemonRepository, "Nidoqueen", "1336", 4);
        populateRaidInfoForBoss(pokemonRepository, "egg4", "1", 4);

        // Tier 5 / EX
        populateRaidInfoForBoss(pokemonRepository, "egg5", "1", 5);
        populateRaidInfoForBoss(pokemonRepository, "Ho-oh", "2222", 5);

        // MEGA
        populateRaidInfoForBoss(pokemonRepository, "egg6", "1", 6);

        // ==== Gen 3+ ====
        // Tier 1
        populateRaidInfoForBoss(pokemonRepository, "Wailmer", "838", 1);
        populateRaidInfoForBoss(pokemonRepository, "Snorunt", "441", 1);
        populateRaidInfoForBoss(pokemonRepository, "Swablu", "470", 1);
        populateRaidInfoForBoss(pokemonRepository, "Shuppet", "581", 1);
        populateRaidInfoForBoss(pokemonRepository, "Duskull", "403", 1);
        populateRaidInfoForBoss(pokemonRepository, "Bulbasaur", "637", 1);
        populateRaidInfoForBoss(pokemonRepository, "Charmander", "560", 1);
        populateRaidInfoForBoss(pokemonRepository, "Squirtle", "540", 1);
        populateRaidInfoForBoss(pokemonRepository, "Makuhita", "467", 1);
        populateRaidInfoForBoss(pokemonRepository, "Meditite", "396", 1);
        populateRaidInfoForBoss(pokemonRepository, "Shinx", "500", 1);
        populateRaidInfoForBoss(pokemonRepository, "Buizel", "602", 1);
        populateRaidInfoForBoss(pokemonRepository, "Mareep", "566", 1);
        populateRaidInfoForBoss(pokemonRepository, "Magnemite", "778", 1);
        populateRaidInfoForBoss(pokemonRepository, "Minun", "968", 1);
        populateRaidInfoForBoss(pokemonRepository, "Plusle", "1016", 1);
        populateRaidInfoForBoss(pokemonRepository, "Wingull", "437", 1);
        populateRaidInfoForBoss(pokemonRepository, "Dratini", "574", 1);
        populateRaidInfoForBoss(pokemonRepository, "Feebas", "157", 1);
        populateRaidInfoForBoss(pokemonRepository, "Lotad", "342", 1);
        populateRaidInfoForBoss(pokemonRepository, "Sunkern", "226", 1);
        populateRaidInfoForBoss(pokemonRepository, "Chikorita", "534", 1);
        populateRaidInfoForBoss(pokemonRepository, "Skorupi", "576", 1);
        populateRaidInfoForBoss(pokemonRepository, "Kricketot", "229", 1);
        populateRaidInfoForBoss(pokemonRepository, "Caterpie", "249", 1);
        populateRaidInfoForBoss(pokemonRepository, "Drifloon", "684", 1);
        populateRaidInfoForBoss(pokemonRepository, "Bagon", "660", 1);
        populateRaidInfoForBoss(pokemonRepository, "Bronzor", "344", 1);
        populateRaidInfoForBoss(pokemonRepository, "Horsea", "603", 1);
        populateRaidInfoForBoss(pokemonRepository, "Nidoran", "491", 1);
        populateRaidInfoForBoss(pokemonRepository, "Murkrow", "892", 1);
        populateRaidInfoForBoss(pokemonRepository, "Sentret", "353", 1);
        populateRaidInfoForBoss(pokemonRepository, "Snubbull", "656", 1);
        populateRaidInfoForBoss(pokemonRepository, "Patrat", "452", 1);
        populateRaidInfoForBoss(pokemonRepository, "Lillipup", "523", 1);
        populateRaidInfoForBoss(pokemonRepository, "Klink", "546", 1);
        populateRaidInfoForBoss(pokemonRepository, "Beldum", "513", 1);
        populateRaidInfoForBoss(pokemonRepository, "Koffing", "694", 1);
        populateRaidInfoForBoss(pokemonRepository, "Zubat", "381", 1);
        populateRaidInfoForBoss(pokemonRepository, "Ekans", "529", 1);
        populateRaidInfoForBoss(pokemonRepository, "Cubone", "536", 1);
        populateRaidInfoForBoss(pokemonRepository, "Drowzee", "594", 1);
        populateRaidInfoForBoss(pokemonRepository, "Burmy", "279", 1);
        populateRaidInfoForBoss(pokemonRepository, "Ralts", "308", 1);
        populateRaidInfoForBoss(pokemonRepository, "Yanma", "840", 1);
        populateRaidInfoForBoss(pokemonRepository, "Litwick", "575", 1);
        populateRaidInfoForBoss(pokemonRepository, "Pidove", "484", 1);
        populateRaidInfoForBoss(pokemonRepository, "Tepig", "618", 1);
        populateRaidInfoForBoss(pokemonRepository, "Trapinch", "728", 1);
        populateRaidInfoForBoss(pokemonRepository, "Ducklett", "489", 1);
        populateRaidInfoForBoss(pokemonRepository, "Espurr", "719", 1);

        // Tier 2
        populateRaidInfoForBoss(pokemonRepository, "Mawile", "934", 2);
        populateRaidInfoForBoss(pokemonRepository, "Dewgong", "1082", 2);
        populateRaidInfoForBoss(pokemonRepository, "Slowbro", "1418", 2);
        populateRaidInfoForBoss(pokemonRepository, "Manectric", "1337", 2);
        populateRaidInfoForBoss(pokemonRepository, "Sneasel", "1067", 2);
        populateRaidInfoForBoss(pokemonRepository, "Misdreavus", "1100", 2);
        populateRaidInfoForBoss(pokemonRepository, "Lickitung", "806", 2);
        populateRaidInfoForBoss(pokemonRepository, "Venomoth", "1107", 2);
        populateRaidInfoForBoss(pokemonRepository, "Combusken", "841", 2);
        populateRaidInfoForBoss(pokemonRepository, "Primeape", "1203", 2);
        populateRaidInfoForBoss(pokemonRepository, "Kirlia", "481", 2);
        populateRaidInfoForBoss(pokemonRepository, "Roselia", "1068", 2);
        populateRaidInfoForBoss(pokemonRepository, "Lanturn", "1191", 2);
        populateRaidInfoForBoss(pokemonRepository, "Grovyle", "956", 2);
        populateRaidInfoForBoss(pokemonRepository, "Marshtomp", "1015", 2);
        populateRaidInfoForBoss(pokemonRepository, "Monferno", "899", 2);
        populateRaidInfoForBoss(pokemonRepository, "Pineco", "633", 2);
        populateRaidInfoForBoss(pokemonRepository, "Masquerain", "1297", 2);
        populateRaidInfoForBoss(pokemonRepository, "Combee", "282", 2);
        populateRaidInfoForBoss(pokemonRepository, "Gligar", "1061", 2);
        populateRaidInfoForBoss(pokemonRepository, "Houndour", "705", 2);
        populateRaidInfoForBoss(pokemonRepository, "Feebas", "157", 2);
        populateRaidInfoForBoss(pokemonRepository, "Yamask", "561", 2);
        populateRaidInfoForBoss(pokemonRepository, "Anorith", "874", 2);
        populateRaidInfoForBoss(pokemonRepository, "Lileep", "738", 2);
        populateRaidInfoForBoss(pokemonRepository, "Kingler", "1616", 2);
        populateRaidInfoForBoss(pokemonRepository, "Magneton", "1420", 2);
        populateRaidInfoForBoss(pokemonRepository, "Grotle", "1080", 2);
        populateRaidInfoForBoss(pokemonRepository, "Grumpig", "1354", 2);
        populateRaidInfoForBoss(pokemonRepository, "Swellow", "1097", 2);
        populateRaidInfoForBoss(pokemonRepository, "Fearow", "1141", 2);

        // Tier 3
        populateRaidInfoForBoss(pokemonRepository, "Azumarill", "849", 3);
        populateRaidInfoForBoss(pokemonRepository, "Aerodactyl", "1490", 3);
        populateRaidInfoForBoss(pokemonRepository, "Jynx", "1435", 3);
        populateRaidInfoForBoss(pokemonRepository, "Piloswine", "1305", 3);
        populateRaidInfoForBoss(pokemonRepository, "Starmie", "1476", 3);
        populateRaidInfoForBoss(pokemonRepository, "Claydol", "1126", 3);
        populateRaidInfoForBoss(pokemonRepository, "Pinsir", "1583", 3);
        populateRaidInfoForBoss(pokemonRepository, "Granbull", "1394", 3);
        populateRaidInfoForBoss(pokemonRepository, "Aerodactyl", "1490", 3);
        populateRaidInfoForBoss(pokemonRepository, "Kabutops", "1438", 3);
        populateRaidInfoForBoss(pokemonRepository, "Onix", "572", 3);
        populateRaidInfoForBoss(pokemonRepository, "Breloom", "1375", 2);
        populateRaidInfoForBoss(pokemonRepository, "Hitmonlee", "1375", 3);
        populateRaidInfoForBoss(pokemonRepository, "Hitmonchan", "1199", 3);
        populateRaidInfoForBoss(pokemonRepository, "Raichu", "1247", 3);
        populateRaidInfoForBoss(pokemonRepository, "Donphan", "1722", 3);
        populateRaidInfoForBoss(pokemonRepository, "Tangela", "1262", 3);
        populateRaidInfoForBoss(pokemonRepository, "Sharpedo", "1246", 3);
        populateRaidInfoForBoss(pokemonRepository, "Skarmory", "1204", 3);
        populateRaidInfoForBoss(pokemonRepository, "Espeon", "1811", 3);
        populateRaidInfoForBoss(pokemonRepository, "Umbreon", "1221", 3);
        populateRaidInfoForBoss(pokemonRepository, "Crawdaunt", "1413", 3);
        populateRaidInfoForBoss(pokemonRepository, "Lunatone", "1330", 3);
        populateRaidInfoForBoss(pokemonRepository, "Solrock", "1330", 3);
        populateRaidInfoForBoss(pokemonRepository, "Shuckle", "231", 3);
        populateRaidInfoForBoss(pokemonRepository, "Skuntank", "1347", 3);
        populateRaidInfoForBoss(pokemonRepository, "Persian", "965", 3);
        populateRaidInfoForBoss(pokemonRepository, "Mantine", "1204", 3);
        populateRaidInfoForBoss(pokemonRepository, "Pidgeot", "1216", 3);
        populateRaidInfoForBoss(pokemonRepository, "Hippowdon", "1763", 3);
        populateRaidInfoForBoss(pokemonRepository, "Meganium", "1377", 3);

        // Tier 4
        populateRaidInfoForBoss(pokemonRepository, "Feraligatr", "1554", 4);
        populateRaidInfoForBoss(pokemonRepository, "Absol", "1443", 4);
        populateRaidInfoForBoss(pokemonRepository, "Aggron", "1714", 4);
        populateRaidInfoForBoss(pokemonRepository, "Salamence", "2018", 4);
        populateRaidInfoForBoss(pokemonRepository, "Walrein", "1489", 4);
        populateRaidInfoForBoss(pokemonRepository, "Houndoom", "1445", 4);
        populateRaidInfoForBoss(pokemonRepository, "Togetic", "881", 4);
        populateRaidInfoForBoss(pokemonRepository, "Metagross", "2166", 4);
        populateRaidInfoForBoss(pokemonRepository, "Shiftry", "1333", 4);
        populateRaidInfoForBoss(pokemonRepository, "Ursaring", "1682", 4);
        populateRaidInfoForBoss(pokemonRepository, "Ninjask", "1125", 4);
        populateRaidInfoForBoss(pokemonRepository, "Registeel", "1398", 4);
        populateRaidInfoForBoss(pokemonRepository, "Regice", "1784", 4);
        populateRaidInfoForBoss(pokemonRepository, "Regirock", "1784", 4);
        populateRaidInfoForBoss(pokemonRepository, "Meowth", "427", 4);
        populateRaidInfoForBoss(pokemonRepository, "Excadrill", "1853", 4);
        populateRaidInfoForBoss(pokemonRepository, "Staraptor", "1614", 4);
        populateRaidInfoForBoss(pokemonRepository, "Dragonite", "2167", 4);

        // Tier 5
        populateRaidInfoForBoss(pokemonRepository, "Groudon", "2328", 5);
        populateRaidInfoForBoss(pokemonRepository, "Latios", "2082", 5);
        populateRaidInfoForBoss(pokemonRepository, "Kyogre", "2328", 5);
        populateRaidInfoForBoss(pokemonRepository, "Rayquaza", "2083", 5);
        populateRaidInfoForBoss(pokemonRepository, "Latias", "1929", 5);
        populateRaidInfoForBoss(pokemonRepository, "Mew", "1766", 5);
        populateRaidInfoForBoss(pokemonRepository, "Celebi", "1766", 5);
        populateRaidInfoForBoss(pokemonRepository, "Giratina", "2105", 5);
        populateRaidInfoForBoss(pokemonRepository, "Palkia", "2280", 5);
        populateRaidInfoForBoss(pokemonRepository, "Dialga", "2307", 5);
        populateRaidInfoForBoss(pokemonRepository, "Cresselia", "1633", 5);
        populateRaidInfoForBoss(pokemonRepository, "Heatran", "2145", 5);
        populateRaidInfoForBoss(pokemonRepository, "Deoxys", "1645", 5);
        populateRaidInfoForBoss(pokemonRepository, "Uxie", "1442", 5);
        populateRaidInfoForBoss(pokemonRepository, "Mesprit", "1669", 5);
        populateRaidInfoForBoss(pokemonRepository, "Azelf", "1834", 5);
        populateRaidInfoForBoss(pokemonRepository, "Darkrai", "2136", 5);
        populateRaidInfoForBoss(pokemonRepository, "Cobalion", "1727", 5);
        populateRaidInfoForBoss(pokemonRepository, "Terrakion", "2113", 5);
        populateRaidInfoForBoss(pokemonRepository, "Virizion", "1727", 5);
        populateRaidInfoForBoss(pokemonRepository, "Regigigas", "2478", 5);
        populateRaidInfoForBoss(pokemonRepository, "Tornadus", "1911", 5);
        populateRaidInfoForBoss(pokemonRepository, "Thundurus", "1911", 5);
        populateRaidInfoForBoss(pokemonRepository, "Landorus", "2050", 5);
        populateRaidInfoForBoss(pokemonRepository, "Zacian", "2188", 5);
        populateRaidInfoForBoss(pokemonRepository, "Zamazenta", "2188", 5);

        LOGGER.info("Configured " + pokemonRaidInfo.size() + " raid boss information entries.");
    }

    private void populateRaidInfoForBoss(PokemonRepository pokemonRepository, String pokemonName, String maxCp, int bossTier) {
        final Pokemon pokemon = pokemonRepository.getByName(pokemonName.toUpperCase());
        if (pokemon == null) {
            LOGGER.warn("Exception when getting pokemon by name " + pokemonName + " - needs to be added to repo data file.");
            return;
        }
        pokemonRaidInfo.put(pokemonName.toUpperCase(), new PokemonRaidInfo(pokemon, maxCp, bossTier));
    }

    public RaidBossCounters getCounters(Pokemon pokemon) {
        Validate.notNull(pokemon, "Input pokemon cannot be null!");
        final RaidBossCounters counters = this.counters.get(pokemon.getName().toUpperCase());
        return counters;
    }

    public String getMaxCp(Pokemon pokemon) {
        final PokemonRaidInfo pokemonRaidInfo = this.pokemonRaidInfo.get(pokemon.getName().toUpperCase());
        if (pokemonRaidInfo == null) {
            return null;
        }
        final String maxCp = String.valueOf(pokemonRaidInfo.getMaxCp());
        if (maxCp != null && (!maxCp.isEmpty())) {
            return maxCp;
        } else {
            return null;
        }
    }

    public PokemonRaidInfo getRaidInfo(Pokemon pokemon) {
        final String pokemonName = pokemon.getName().toUpperCase();
        return pokemonRaidInfo.get(pokemonName);
    }
} @Transactional
public class RaidRepository {
    private static final Logger LOGGER = LoggerFactory.getLogger(RaidRepository.class);

    private ClockService clockService;
    private LocaleService localeService;
    private RaidEntityRepository raidEntityRepository;
    private PokemonRepository pokemonRepository;
    private GymRepository gymRepository;
    private TrackingService trackingService;

    // Byte code instrumentation
    protected RaidRepository() {
    }

    @Autowired
    public RaidRepository(ClockService clockService, LocaleService localeService,
                          RaidEntityRepository raidEntityRepository, PokemonRepository pokemonRepository,
                          GymRepository gymRepository, TrackingService trackingService) {
        this.clockService = clockService;
        this.localeService = localeService;
        this.raidEntityRepository = raidEntityRepository;
        this.pokemonRepository = pokemonRepository;
        this.gymRepository = gymRepository;
        this.trackingService = trackingService;
        removeAllExpiredRaids();
    }

    public String executeSignUpCommand(Config config,
                                       User user,
                                       Locale localeForUser,
                                       String[] args,
                                       String help) {
        String people = args[0];
        String userName = user.getName();
        if (args.length < 3 || args.length > 10) {
            throw new WrongNumberOfArgumentsException(user, localeService, 3, args.length, help);
        }
        Integer numberOfPeople = Utils.assertNotTooManyOrNoNumber(user, localeService, people);

        String timeString = args[1];

        StringBuilder gymNameBuilder = new StringBuilder();
        for (int i = 2; i < args.length; i++) {
            gymNameBuilder.append(args[i]).append(" ");
        }
        String gymName = gymNameBuilder.toString().trim();
        final Gym gym = gymRepository.search(user, gymName, config.getRegion());
        final Raid raid = getActiveRaidOrFallbackToExRaid(gym, config.getRegion(), user);

        LocalTime eta = Utils.parseTime(user, timeString, localeService);
        LocalDateTime realEta = LocalDateTime.of(raid.getEndOfRaid().toLocalDate(), eta);

        assertEtaNotAfterRaidEnd(user, raid, realEta, localeService);
        assertSignupTimeNotBeforeRaidStartAndNow(user, realEta, raid.getEndOfRaid(), localeService, raid.isExRaid());

        raid.signUp(user, numberOfPeople, eta, this);
        final String currentSignupText = localeService.getMessageFor(LocaleService.CURRENT_SIGNUPS, localeForUser);
        final Set<SignUp> signUps = raid.getSignUps();
        Set<String> signUpNames = Utils.getNamesOfThoseWithSignUps(signUps, true);
        final String allSignUpNames = StringUtils.join(signUpNames, ", ");
        final String signUpText = raid.getSignUps().size() > 1 ? currentSignupText + "\n" + allSignUpNames : "";
        return localeService.getMessageFor(LocaleService.SIGNUPS, localeForUser, userName,
                gym.getName(), signUpText);
    }

    public String executeUnsignCommand(Config config,
                                       User user,
                                       Locale localeForUser,
                                       String[] args,
                                       String help) {
        String people = args[0];
        String userName = user.getName();
        if (args.length < 2 || args.length > 10) {
            throw new WrongNumberOfArgumentsException(user, localeService, 2, args.length, help);
        }
        Integer numberOfPeople = Utils.assertNotTooManyOrNoNumber(user, localeService, people);

        StringBuilder gymNameBuilder = new StringBuilder();
        for (int i = 1; i < args.length; i++) {
            gymNameBuilder.append(args[i]).append(" ");
        }
        String gymName = gymNameBuilder.toString().trim();
        final Gym gym = gymRepository.search(user, gymName, config.getRegion());
        Raid raid = getActiveRaidOrFallbackToExRaid(gym, config.getRegion(), user);
        final RaidEntitySignUp signUp = findEntityByRaidId(raid.getId()).getSignUp(user.getName());
        if (signUp == null) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.NO_SIGNUP_AT_GYM,
                    localeForUser, user.getName(), gym.getName()));
        }

        raid = removeFromSignUp(raid.getId(), user, 0, 0, 0, numberOfPeople,
                LocalDateTime.of(raid.getEndOfRaid().toLocalDate(),
                        signUp.getArrivalTime()));
        final String currentSignupText = localeService.getMessageFor(LocaleService.CURRENT_SIGNUPS, localeForUser);
        final Set<SignUp> signUps = raid.getSignUps();
        Set<String> signUpNames = Utils.getNamesOfThoseWithSignUps(signUps, true);
        final String allSignUpNames = StringUtils.join(signUpNames, ", ");
        final String signUpText = raid.getSignUps().size() > 1 ? currentSignupText + "\n" + allSignUpNames : "";
        return localeService.getMessageFor(LocaleService.UNSIGN, localeForUser, userName,
                gym.getName(), signUpText);
    }

    public Raid newRaid(User raidCreator, Raid raid, Guild guild, Config config, String rawMessage) {
        RaidEntity raidEntity = getActiveOrFallbackToExRaidEntity(raid.getGym(), raid.getRegion());

        if (raidEntity != null) {
            if ((raidEntity.isExRaid() && !raid.isExRaid()) || (!raidEntity.isExRaid() && raid.isExRaid())) {
                // This is a situation we should allow, in case Niantic now allows for raids to happen at EX gyms
            } else {
                throw new RaidExistsException(raidCreator, getRaidInstance(raidEntity),
                        localeService, localeService.getLocaleForUser(raidCreator));
            }
        }

        final Raid raidInstance = getRaidInstance(saveRaid(raidCreator, raid));
        trackingService.notifyTrackers(guild, raidInstance, config, raidCreator, rawMessage);
        return raidInstance;
    }

    private RaidEntity findEntityByRaidId(String raidId) {
        final RaidEntity raidEntity = raidId == null ? null : raidEntityRepository.findOne(raidId);
        removeRaidIfExpired(raidEntity);
        return raidEntity;
    }

    private RaidEntity saveRaid(User raidCreator, Raid raid) {
        final RaidEntity toBeSaved = new RaidEntity(UUID.randomUUID().toString(),
                raid.getPokemon().getName(),
                raid.getEndOfRaid(),
                raid.getGym().getName(),
                raidCreator.getName(),
                raid.getRegion(),
                raid.isExRaid());
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Creating raid: " + toBeSaved);
        }
        return raidEntityRepository.save(toBeSaved);

    }

    private Raid getRaidInstance(RaidEntity raidEntity) {
        Validate.notNull(raidEntity);
        final String region = raidEntity.getRegion();
        Pokemon pokemon = pokemonRepository.getByName(raidEntity.getPokemon());
        if (pokemon == null) {
            LOGGER.warn("Could not find pokemon for this raid in repository: " + raidEntity.getPokemon());
        }
        Gym gym = gymRepository.findByName(raidEntity.getGym(), region);
        if (gym == null) {
            LOGGER.warn("Could not find gym for this raid in repository: " + raidEntity.getGym());
        }
        final Raid raid = new Raid(pokemon,
                raidEntity.getEndOfRaid(),
                gym, localeService, region, raidEntity.isExRaid());
        raid.setCreator(raidEntity.getCreator());
        raid.setId(raidEntity.getId());
        Map<String, SignUp> signUps = new ConcurrentHashMap<>();
        for (RaidEntitySignUp signUp : raidEntity.getSignUpsAsSet()) {
            signUps.put(signUp.getResponsible(), new SignUp(signUp.getResponsible(), signUp.getNumberOfPeople(),
                    LocalTime.parse(signUp.getEta(), Utils.timeParseFormatter)));
        }
        raid.setSignUps(signUps);
        return raid;
    }

    public Raid getActiveRaidOrFallbackToExRaid(Gym gym, String region, User user) {
        RaidEntity raidEntity = getActiveOrFallbackToExRaidEntity(gym, region);
        if (raidEntity == null) {
            throw new RaidNotFoundException(gym, localeService, user);
        }
        final Raid raid = getRaidInstance(raidEntity);
        return raid;
    }

    private RaidEntity getActiveOrFallbackToExRaidEntity(Gym gym, String region) {
        RaidEntity raidEntity = null;
        List<RaidEntity> raidEntities = raidEntityRepository.findByGymAndRegionOrderByEndOfRaidAsc(gym.getName(), region);
        RaidEntity exEntity = null;
        for (RaidEntity entity : raidEntities) {
            if (entity.isExpired(clockService)) {
                LOGGER.info("Removing expired raid: " + entity.getId());
                raidEntityRepository.delete(entity);
            } else if (entity.isExRaid()) {
                exEntity = entity;
                break;
            } else {
                if (raidEntity != null) {
                    throw new IllegalStateException("Raid state in database seems off. " +
                            "Please notify the bot developer so it can be checked: " + raidEntity);
                }
                raidEntity = entity;
            }
        }

        if (raidEntity == null) {
            if (exEntity != null) {
                raidEntity = exEntity;
            }
        }
        return raidEntity;
    }

    public Set<Raid> getAllRaidsForRegion(String region) {
        removeExpiredRaids(region);
        List<RaidEntity> raidEntityList = raidEntityRepository.findByRegionOrderByPokemonAscEndOfRaidAsc(region);
        Set<Raid> activeRaids = new LinkedHashSet<>();
        for (RaidEntity entity : raidEntityList) {
            activeRaids.add(getRaidInstance(entity));
        }
        return activeRaids;
    }

    private void removeAllExpiredRaids() {
        List<RaidEntity> raidEntityList = raidEntityRepository.findAll();
        for (RaidEntity entity : raidEntityList) {
            removeRaidIfExpired(entity);
        }
    }

    private void removeExpiredRaids(String region) {
        List<RaidEntity> raidEntityList = raidEntityRepository.findByRegion(region);
        for (RaidEntity entity : raidEntityList) {
            removeRaidIfExpired(entity);
        }
    }

    // Returns null if raid is expired
    private RaidEntity removeRaidIfExpired(RaidEntity raidEntity) {
        if (raidEntity != null && raidEntity.isExpired(clockService)) {
            final String id = raidEntity.getId();
            // Clean up expired raid
            raidEntityRepository.delete(raidEntity);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Removed expired raid with ID: " + id);
            }
            return null;
        } else {
            return raidEntity;
        }
    }

    public void addSignUp(User user, Raid raid, SignUp theSignUp) {
        RaidEntity entity = findEntityByRaidId(raid.getId());

        RaidEntitySignUp entitySignUp = entity.getSignUp(user.getName());
        if (entitySignUp == null) {
            entity.addSignUp(new RaidEntitySignUp(user.getName(), theSignUp.getHowManyPeople(),
                    Utils.printTime(theSignUp.getArrivalTime())));
        } else {
            entitySignUp.setNumberOfPeople(theSignUp.getHowManyPeople(), localeService, user);
            entitySignUp.setEta(Utils.printTime(theSignUp.getArrivalTime()));
        }
        raidEntityRepository.save(entity);
    }

    public void removeSignUp(User user, Raid raid, SignUp theSignUp) {
        RaidEntity entity = findEntityByRaidId(raid.getId());
        entity.removeSignUp(new RaidEntitySignUp(user.getName(), theSignUp.getHowManyPeople(),
                Utils.printTime(theSignUp.getArrivalTime())));
        raidEntityRepository.save(entity);
    }

    public Set<Raid> getRaidsInRegionForPokemon(String region, Pokemon pokemon) {
        removeExpiredRaids(region);
        List<RaidEntity> raidEntityList = raidEntityRepository.findByPokemonAndRegionOrderByEndOfRaidAsc(pokemon.getName(), region);
        Set<Raid> activeRaids = new LinkedHashSet<>();
        for (RaidEntity entity : raidEntityList) {
            activeRaids.add(getRaidInstance(entity));
        }
        return activeRaids;
    }

    public Raid changePokemon(Raid raid, Pokemon pokemon, Guild guild, Config config, User user, String rawMessage) {
        RaidEntity raidEntity = getActiveOrFallbackToExRaidEntity(raid.getGym(), raid.getRegion());
        if (!raidEntity.getPokemon().equalsIgnoreCase(raid.getPokemon().getName())) {
            throw new IllegalStateException("Database issues. Please notify the developer: " +
                    "magnus.mickelsson@gmail.com and describe what happened.");
        }
        raidEntity.setPokemon(pokemon.getName());
        raidEntity = raidEntityRepository.save(raidEntity);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Changed pokemon for raid " + raid + " to " + pokemon + ".");
        }
        final Raid raidInstance = getRaidInstance(raidEntity);
        trackingService.notifyTrackers(guild, raidInstance, config, user, rawMessage);
        return raidInstance;
    }

    public Raid changeEndOfRaid(String raidId, LocalDateTime newEndOfRaid, Guild guild, Config config,
                                User user, String rawMessage) {
        RaidEntity raidEntity = findEntityByRaidId(raidId);
        raidEntity.setEndOfRaid(newEndOfRaid);
        raidEntity = raidEntityRepository.save(raidEntity);
        final Raid raidInstance = getRaidInstance(raidEntity);
        trackingService.notifyTrackers(guild, raidInstance, config, user, rawMessage);
        return raidInstance;
    }

    public boolean delete(Raid raid) {
        RaidEntity raidEntity = getActiveOrFallbackToExRaidEntity(raid.getGym(), raid.getRegion());
        if (raidEntity != null) {
            raidEntityRepository.delete(raidEntity);
            return true;
        } else {
            return false;
        }
    }

    public Raid getById(String id) {
        final RaidEntity entity = raidEntityRepository.findOne(id);
        return getRaidInstance(entity);
    }

    public Raid modifySignUp(String raidId, User user, int mystic, int instinct, int valor, int plebs,
                             LocalDateTime startAt) {
        RaidEntity raidEntity = findEntityByRaidId(raidId);
        RaidEntitySignUp signUp = raidEntity.getSignUp(user.getName());
        final String startAtTime = Utils.printTime(startAt.toLocalTime());
        if (signUp == null) {
            final int sum = mystic + instinct + valor + plebs;
            assertSumNotLessThanOne(user, sum);
            raidEntity.addSignUp(new RaidEntitySignUp(user.getName(), sum, startAtTime));
        } else {
            int sum = signUp.getNumberOfPeople();
            if (startAt.toLocalTime().equals(Utils.parseTime(user, signUp.getEta(), localeService))) {
                sum = sum + mystic + instinct + valor + plebs;
            } else {
                signUp.setEta(startAtTime);
                // Reset number of signups to what the input gives since we changed time
                sum = mystic + instinct + valor + plebs;
            }
            assertSumNotLessThanOne(user, sum);
            signUp.setNumberOfPeople(sum, localeService, user);
        }
        raidEntity = raidEntityRepository.save(raidEntity);

        return getRaidInstance(raidEntity);
    }

    private void assertSumNotLessThanOne(User user, int sum) {
        if (sum <= 0) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.ERROR_PARSE_PLAYERS,
                    localeService.getLocaleForUser(user),
                    "" + sum, String.valueOf(HIGH_LIMIT_FOR_SIGNUPS)));
        }
    }

    public Raid removeFromSignUp(String raidId, User user, int mystic, int instinct, int valor, int plebs,
                                 LocalDateTime startAt) {
        RaidEntity raidEntity = findEntityByRaidId(raidId);
        if (raidEntity == null) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAID_AT_GYM, localeService.getLocaleForUser(user)));
        }
        RaidEntitySignUp signUp = raidEntity.getSignUp(user.getName());
        final String startAtTime = Utils.printTime(startAt.toLocalTime());
        if (signUp == null) {
            // Ignore this case, when there is no signup to remove from. Silent ignore.
        } else if (startAtTime.equals(signUp.getEta())) {
            final int sum = signUp.getNumberOfPeople() - mystic - instinct - valor - plebs;
            if (sum <= 0) {
                // Remove signup
                raidEntity.removeSignUp(signUp);
            } else {
                signUp.setNumberOfPeople(sum, localeService, user);
            }
            raidEntity = raidEntityRepository.save(raidEntity);
        } else {
            // Ignore if they're trying to remove signups for a group they're no longer signed up for - we let them untick their emote
        }
        return getRaidInstance(raidEntity);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Raid removeAllSignUpsAt(String raidId, LocalDateTime startAt) {
        Validate.notNull(raidId, "Raid ID cannot be null");
        Validate.notNull(startAt, "Start time cannot be null");
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("About to remove signups for raid " + raidId + " at " + printTimeIfSameDay(startAt));
        }
        RaidEntity entity = findEntityByRaidId(raidId);
        if (entity != null) {
            for (RaidEntitySignUp signUp : entity.getSignUpsAsSet()) {
                if (signUp.getArrivalTime().equals(startAt.toLocalTime())) {
                    RaidEntitySignUp removed = entity.removeSignUp(signUp);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Removed signup: " + removed);
                    }
                }
            }
            entity = raidEntityRepository.save(entity);
        }

        return getRaidInstance(entity);
    }

    public void moveAllSignUpsForTimeToNewTime(String raidId, LocalDateTime currentStartAt, LocalDateTime newDateTime, User user) {
        Validate.notNull(raidId, "Raid ID cannot be null");
        Validate.notNull(currentStartAt, "Current start time cannot be null");
        Validate.notNull(newDateTime, "New start time cannot be null");
        Validate.notNull(user, "User cannot be null");
        RaidEntity entity = raidEntityRepository.findOne(raidId);
        if (entity != null) {
            for (RaidEntitySignUp signUp : entity.getSignUpsAsSet()) {
                if (signUp.getArrivalTime().equals(currentStartAt.toLocalTime())) {
                    signUp.setEta(Utils.printTime(newDateTime.toLocalTime()));
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Changed ETA for signup: " + signUp);
                    }
                }
            }
            raidEntityRepository.save(entity);
        } else {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAID_AT_GYM, localeService.getLocaleForUser(user)));
        }
    }

    public RaidGroup newGroupForRaid(User user, RaidGroup group, Raid raid, Guild guild, Config config) {
        Validate.notNull(user, "User");
        Validate.notNull(group, "Group");
        Validate.notNull(raid, "Raid");
        Validate.notEmpty(raid.getId(), "Raid ID");
        RaidEntity raidEntity = findEntityByRaidId(raid.getId());
        final boolean added = raidEntity.addGroup(group);
        if (!added) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.GROUP_NOT_ADDED,
                    localeService.getLocaleForUser(user), String.valueOf(raid)));
        }
        // todo: special message for new group?
//        trackingService.notifyTrackers(guild, raid, config, user);

        return group;
    }

    public List<RaidGroup> getGroupsForServer(String server) {
        Validate.notEmpty(server, "Server is empty");
        return raidEntityRepository.findGroupsForServer(server);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public RaidGroup deleteGroupInNewTransaction(String raidId, String groupId) {
        final RaidEntity entity = findEntityByRaidId(raidId);
        if (entity != null) {
            final RaidGroup removedGroup = entity.removeGroup(groupId);
            if (removedGroup == null) {
                LOGGER.debug("Didn't delete group, it has already been been deleted.");
            }
            return removedGroup;
        } else {
            LOGGER.debug("Didn't delete group, raid seems to have been deleted.");
            return null;
        }
    }

    public RaidGroup deleteGroup(String raidId, String groupId) {
        final RaidEntity entity = findEntityByRaidId(raidId);
        if (entity == null) {
            return null;
        }
        final RaidGroup removedGroup = entity.removeGroup(groupId);
        if (removedGroup == null) {
            throw new RuntimeException("No group with ID " + groupId + " for raid " + entity);
        }
        return removedGroup;
    }

    public boolean userHasGroupForRaid(User user, Raid raid) {
        RaidEntity entity = findEntityByRaidId(raid.getId());
        if (entity.userHasGroup(user)) {
            return true;
        } else {
            return false;
        }
    }

    public boolean hasGroupForRaid(User user, Raid raid, LocalDateTime startAt) {
        RaidEntity entity = findEntityByRaidId(raid.getId());
        if (entity.hasGroupAt(user, startAt)) {
            return true;
        } else {
            return false;
        }
    }

    public boolean existsGroupForRaidAt(Raid raid, LocalDateTime startAt) {
        RaidEntity entity = findEntityByRaidId(raid.getId());
        if (entity.existsGroupAt(startAt)) {
            return true;
        } else {
            return false;
        }
    }

    public boolean hasManyGroupsForRaid(User user, Raid raid) {
        RaidEntity entity = findEntityByRaidId(raid.getId());
        if (entity.hasManyGroupsBy(user)) {
            return true;
        } else {
            return false;
        }
    }

    public Set<RaidGroup> getGroups(Raid raid) {
        RaidEntity entity = findEntityByRaidId(raid.getId());
        final Set<RaidGroup> groups = entity.getGroupsAsSet();
        return groups;
    }

    public String listGroupsForRaid(Raid raid, Set<RaidGroup> groups) {
        StringBuilder sb = new StringBuilder();
        if (groups.size() > 0) {
            sb.append(" " + Emotes.GROUP + " ");
            Set<String> times = new LinkedHashSet<>();
            int signUpsInGroups = 0;
            for (RaidGroup group : groups) {
                final LocalTime groupTime = group.getStartsAt().toLocalTime();
                final int numberOfPeopleInGroup = countSignups(raid.getSignUpsAt(groupTime));
                times.add(printTime(groupTime) + " (**" + numberOfPeopleInGroup + "**)");
                signUpsInGroups += numberOfPeopleInGroup;
            }
            sb.append(StringUtils.join(times, ", "));
            RaidEntity entity = findEntityByRaidId(raid.getId());
            final Set<RaidEntitySignUp> signUpsAsSet = entity.getSignUpsAsSet();
            final long totalSignUps = signUpsAsSet.stream().mapToInt(RaidEntitySignUp::getNumberOfPeople).sum();
            if (totalSignUps > signUpsInGroups) {
                sb.append(", -:- (**").append(totalSignUps - signUpsInGroups).append("**)");
            }
        }
        return sb.toString();
    }

    private int countSignups(Set<SignUp> signUpsAt) {
        int i = 0;
        for (SignUp s : signUpsAt) {
            i += s.getHowManyPeople();
        }
        return i;
    }

    public RaidGroup changeGroup(User user, String raidId, String groupCreatorId, LocalDateTime currentStartAt,
                                 LocalDateTime newDateTime) {
        final RaidEntity entityByRaidId = findEntityByRaidId(raidId);
        if (entityByRaidId == null) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAID_AT_GYM, localeService.getLocaleForUser(user)));
        }
        RaidGroup group = entityByRaidId.getGroupByCreatorAndStart(groupCreatorId, currentStartAt);
        if (group == null) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.NO_SUCH_GROUP,
                    localeService.getLocaleForUser(user)));
        }
        group.setStartsAt(newDateTime);
        raidEntityRepository.save(entityByRaidId);
        // todo: notify !raid track listeners?

        return group;
    }

    // todo: do a select count(*) instead of actually getting the list
    public boolean isActiveRaidAt(Gym raidGym, String region) {
        List<RaidEntity> entities =
                raidEntityRepository.findByGymAndRegionOrderByEndOfRaidAsc(raidGym.getName(), region);

        if (entities.size() == 0) {
            return false;
        }

        for (RaidEntity entity : entities) {
            if (!entity.isExRaid()) {
                return true;
            }
        }
        return false;
    }

    public boolean isActiveOrExRaidAt(Gym raidGym, String region) {
        List<RaidEntity> entities =
                raidEntityRepository.findByGymAndRegionOrderByEndOfRaidAsc(raidGym.getName(), region);

        if (entities.size() == 0) {
            return false;
        } else {
            return true;
        }
    }
} public class EmoticonSignUpMessageListener implements EventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(EmoticonSignUpMessageListener.class);
    private final BotService botService;
    private final LocaleService localeService;
    private final ServerConfigRepository serverConfigRepository;
    private final RaidRepository raidRepository;
    private final PokemonRepository pokemonRepository;
    private final GymRepository gymRepository;
    private String emoteMessageId;
    private final String raidId;
    private String infoMessageId;
    private LocalDateTime startAt;
    private String userHadError = null;
    private String userId;

    public EmoticonSignUpMessageListener(BotService botService, LocaleService localeService,
                                         ServerConfigRepository serverConfigRepository,
                                         RaidRepository raidRepository, PokemonRepository pokemonRepository,
                                         GymRepository gymRepository,
                                         String raidId, LocalDateTime startAt, User user) {
        this.botService = botService;
        this.localeService = localeService;
        this.serverConfigRepository = serverConfigRepository;
        this.raidRepository = raidRepository;
        this.pokemonRepository = pokemonRepository;
        this.gymRepository = gymRepository;
        this.raidId = raidId;
        this.startAt = startAt;
        this.userId = user.getId();
        botService.addEmoticonEventListener(this);
    }

    public EmoticonSignUpMessageListener(BotService botService, LocaleService localeService,
                                         ServerConfigRepository serverConfigRepository,
                                         RaidRepository raidRepository, PokemonRepository pokemonRepository,
                                         GymRepository gymRepository,
                                         String raidId, LocalDateTime startAt, String userId) {
        this.botService = botService;
        this.localeService = localeService;
        this.serverConfigRepository = serverConfigRepository;
        this.raidRepository = raidRepository;
        this.pokemonRepository = pokemonRepository;
        this.gymRepository = gymRepository;
        this.raidId = raidId;
        this.startAt = startAt;
        this.userId = userId;
        botService.addEmoticonEventListener(this);
    }

    public void setEmoteMessageId(String emoteMessageId) {
        this.emoteMessageId = emoteMessageId;
    }

    public void setInfoMessageId(String infoMessageId) {
        this.infoMessageId = infoMessageId;
    }

    @Override
    public void onEvent(GenericEvent event) {
        String reactionMessageId = null;
        User user = null;
        if (emoteMessageId == null && infoMessageId == null) {
            LOGGER.trace("This listener haven't received a emote message id or info message id yet.");
            return;
        }
        try {
            if (event instanceof GuildMessageReactionAddEvent) {
                final GuildMessageReactionAddEvent reactionEvent = (GuildMessageReactionAddEvent) event;
                user = reactionEvent.getUser();
                // If the bot added any reactions, don't respond to them
                if (user.isBot()) return;
                reactionMessageId = reactionEvent.getReaction().getMessageId();
                if (emoteMessageId == null || !emoteMessageId.equals(reactionMessageId)) {
                    return;
                }
                // If this is a reaction for a user that just triggered an error with his/her reaction, skip it
                if (user.getName().equals(userHadError)) {
                    userHadError = null;
                    return;
                }

                final MessageReaction.ReactionEmote emote = reactionEvent.getReaction().getReactionEmote();
                if (emote != null) {
                    switch (emote.getName()) {
                        case Emotes.ONE:
                            addToSignUp(user, 0, 0, 0, 1);
                            break;
                        case Emotes.TWO:
                            addToSignUp(user, 0, 0, 0, 2);
                            break;
                        case Emotes.THREE:
                            addToSignUp(user, 0, 0, 0, 3);
                            break;
                        case Emotes.FOUR:
                            addToSignUp(user, 0, 0, 0, 4);
                            break;
                        case Emotes.FIVE:
                            addToSignUp(user, 0, 0, 0, 5);
                            break;
                        default:
                    }
                }
            } else if (event instanceof GuildMessageReactionRemoveEvent) {
                final GuildMessageReactionRemoveEvent reactionEvent = (GuildMessageReactionRemoveEvent) event;
                // If the bot added any reactions, don't respond to them
                user = reactionEvent.getUser();
                if (user.isBot()) return;
                // If this is a reaction for a user that just triggered an error with his/her reaction, skip it
                if (user.getName().equals(userHadError)) {
                    userHadError = null;
                    return;
                }
                reactionMessageId = reactionEvent.getReaction().getMessageId();
                if (!emoteMessageId.equals(reactionMessageId)) {
                    return;
                }

                final MessageReaction.ReactionEmote emote = reactionEvent.getReaction().getReactionEmote();
                if (emote != null) {
                    switch (emote.getName()) {
                        case Emotes.ONE:
                            removeFromSignUp(user, 0, 0, 0, 1);
                            break;
                        case Emotes.TWO:
                            removeFromSignUp(user, 0, 0, 0, 2);
                            break;
                        case Emotes.THREE:
                            removeFromSignUp(user, 0, 0, 0, 3);
                            break;
                        case Emotes.FOUR:
                            removeFromSignUp(user, 0, 0, 0, 4);
                            break;
                        case Emotes.FIVE:
                            removeFromSignUp(user, 0, 0, 0, 5);
                            break;
                        default:
                    }
                }
            }
        } catch (Throwable t) {
            if (event instanceof GenericGuildMessageReactionEvent) {
                final GenericGuildMessageReactionEvent guildMessageReactionEvent =
                        (GenericGuildMessageReactionEvent) event;
                reactionMessageId = guildMessageReactionEvent.getReaction().getMessageId();
                if (emoteMessageId == null) {
                    LOGGER.warn("Emote message ID = null, this should get cleaned up!" +
                            " Event: " + printInfoAbout(event));
                    return;
                }
                if (!emoteMessageId.equals(reactionMessageId)) {
                    LOGGER.warn("We got a guild reaction event throwing exception, but not one we were listening for!" +
                            " Event: " + printInfoAbout(event));
                    return;
                }
                // Since we got an error, remove last reaction
                if (reactionMessageId != null && reactionMessageId.equals(emoteMessageId)) {
                    // Do this with a slight delay to prevent graphical glitches client side.
                    guildMessageReactionEvent.getReaction().removeReaction(user)
                            .queueAfter(30, TimeUnit.MILLISECONDS);
                    userHadError = user.getName();
                }
                if (user != null && t.getMessage() != null) {
                    MessageBuilder messageBuilder = new MessageBuilder();
                    if (!t.getMessage().contains(user.getAsMention())) {
                        messageBuilder.append(user.getAsMention())
                                .append(": ");
                    }
                    messageBuilder.append(t.getMessage());
                    guildMessageReactionEvent.getChannel().sendMessage(messageBuilder.build()).queue();
                } else {
                    LOGGER.warn("We have a situation where user " + user + " or exception (of type " +
                            t.getClass().getSimpleName() + ") message is null! Event: " +
                            printInfoAbout(event));
                }
            } else {
                LOGGER.warn("Exception in event listener! Event: " +
                        printInfoAbout(event));
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Stacktrace:", t);
                }
            }
        }
    }

    private String printInfoAbout(GenericEvent event) {
        try {
            StringBuilder sb = new StringBuilder();
            if (event == null) {
                return "null";
            }
            String className = event.getClass().getSimpleName();
            sb.append(className);
            String reflectionToString;
            try {
                reflectionToString = ReflectionToStringBuilder.toString(event);
            } catch (Throwable t) {
                reflectionToString = "N/A";
            }
            sb.append(":").append(reflectionToString);
            return sb.toString();
        } catch (Throwable t) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Exception when printing info about event", t);
            }
            return "N/A";
        }
    }

    private Raid addToSignUp(User user, int mystic, int instinct, int valor, int plebs) {
        Raid changedRaid = raidRepository.modifySignUp(raidId, user,
                mystic, instinct, valor, plebs, startAt);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Added signup for user " + user.getName() +
                    " to raid: " + changedRaid + " - mystic: " + mystic + ", instinct: " + instinct + ", valor: " +
                    valor + ", plebs: " + plebs);
        }
        return changedRaid;
    }

    private Raid removeFromSignUp(User user, int mystic, int instinct, int valor, int plebs) {
        Raid changedRaid = raidRepository.removeFromSignUp(raidId, user,
                mystic, instinct, valor, plebs, startAt);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Removed from signup for user " + user.getName() +
                    ", raid: " + changedRaid + " - mystic: " + mystic + ", instinct: " + instinct + ", valor: " +
                    valor + ", plebs: " + plebs);
        }
        return changedRaid;
    }

    public String getEmoteMessageId() {
        return emoteMessageId;
    }

    public String getInfoMessageId() {
        return infoMessageId;
    }

    public void setStartAt(LocalDateTime startAt) {
        this.startAt = startAt;
    }

    public LocalDateTime getStartAt() {
        return startAt;
    }

    public String getRaidId() {
        return raidId;
    }

    public String getUserId() {
        return userId;
    }

    @Override
    public String toString() {
        return "EmoticonSignUpMessageListener{" +
                "emoteMessageId='" + emoteMessageId + '\'' +
                ", raidId='" + raidId + '\'' +
                ", infoMessageId='" + infoMessageId + '\'' +
                ", startAt=" + startAt +
                ", userId='" + userId + '\'' +
                '}';
    }
} @Transactional(propagation = Propagation.REQUIRES_NEW)
public class TrackingService {
    private static final transient Logger LOGGER = LoggerFactory.getLogger(TrackingService.class);
    private final LocaleService localeService;
    private final UserConfigRepository userConfigRepository;
    private final PokemonRepository pokemonRepository;
    private Set<PokemonTrackingTarget> trackingTargets = new ConcurrentSkipListSet<>();

    public TrackingService(LocaleService localeService,
                           UserConfigRepository userConfigRepository,
                           PokemonRepository pokemonRepository) {
        this.localeService = localeService;
        this.userConfigRepository = userConfigRepository;
        this.pokemonRepository = pokemonRepository;
    }

    public void notifyTrackers(Guild guild, Raid raid, Config configForServer, User user, String rawMessage) {
        Validate.notNull(guild, "Guild is null");
        Validate.notNull(raid, "Raid is null");
        Validate.notNull(configForServer, "Config is null");
        Validate.notNull(user, "User is null");
        final Set<PokemonTrackingTarget> trackingTargets = getTrackingTargets();
        for (TrackingTarget t : trackingTargets) {
            if (t.canHandle(configForServer, user, raid, guild)) {
                try {
                    t.handle(guild, localeService, configForServer, user, raid, rawMessage);
                } catch (Throwable e) {
                    LOGGER.debug("Could not handle tracking message for server " + configForServer.getServer() +
                            " and target " +
                            "" + t + " due to an exception: " + e.getMessage());
                }
            }
        }
    }

    public Set<PokemonTrackingTarget> getTrackingTargets() {
        if (trackingTargets.size() == 0) {
            // We most likely had a server restart, load saved tracking from database
            for (UserConfig config : userConfigRepository.findAll()) {
                if (config.getTracking1() != null) {
                    trackingTargets.add(new PokemonTrackingTarget(config.getId(),
                            pokemonRepository.search(config.getTracking1(), null)));
                }
                if (config.getTracking2() != null) {
                    trackingTargets.add(new PokemonTrackingTarget(config.getId(),
                            pokemonRepository.search(config.getTracking2(), null)));
                }
                if (config.getTracking3() != null) {
                    trackingTargets.add(new PokemonTrackingTarget(config.getId(),
                            pokemonRepository.search(config.getTracking3(), null)));
                }
            }
        }
        return trackingTargets;
    }

    public void clearCache() {
        trackingTargets = new ConcurrentSkipListSet<>();
    }

    public void add(Pokemon pokemon, User user, Config config) {
        Validate.notNull(pokemon, "Pokemon");
        Validate.notNull(user, "User");
        Validate.notNull(config, "Config");

        PokemonTrackingTarget trackingTarget = new PokemonTrackingTarget(user.getId(), pokemon);
        if (getTrackingTargets().contains(trackingTarget)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.TRACKING_EXISTS,
                    localeService.getLocaleForUser(user)));
        }
        addToDbAndCollection(trackingTarget, user, config);
    }

    private void addToDbAndCollection(PokemonTrackingTarget trackingTarget, User user, Config config) {
        UserConfig userConfig = userConfigRepository.findOne(user.getId());
        if (userConfig != null) {
            if (userConfig.hasFreeTrackingSpot()) {
                userConfig.setNextTrackingSpot(trackingTarget.getPokemon());
            } else {
                throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.TRACKING_NONE_FREE,
                        localeService.getLocaleForUser(user)));
            }
        } else {
            // Per default, let user have the same locale as the server
            userConfig = new UserConfig(user.getId(), trackingTarget.getPokemon(), null, null,
                    config.getLocale());
        }
        userConfigRepository.save(userConfig);
        trackingTargets.add(trackingTarget);
    }

    public void removeForUser(PokemonTrackingTarget trackingTarget, User user) {
        if (!getTrackingTargets().contains(trackingTarget)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.TRACKING_NOT_EXISTS,
                    localeService.getLocaleForUser(user)));
        }
        removeFromDbAndCollection(trackingTarget, user);
    }

    private void removeFromDbAndCollection(PokemonTrackingTarget trackingTarget, User user) {
        UserConfig userConfig = userConfigRepository.findOne(user.getId());
        if (userConfig != null) {
            userConfig.removeTrackingFor(trackingTarget.getPokemon());
            userConfigRepository.save(userConfig);
        }
        trackingTargets.remove(trackingTarget);
    }

    public void removeAllForUser(User user) {
        trackingTargets.forEach(t -> {
            if (t.getUserId().equals(user.getId())) {
                removeForUser(t, user);
            }
        });
    }
} public class GymHuntrRaidEventListener implements EventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(GymHuntrRaidEventListener.class);

    private ServerConfigRepository serverConfigRepository;
    private RaidRepository raidRepository;
    private GymRepository gymRepository;
    private PokemonRepository pokemonRepository;
    private LocaleService localeService;
    private ExecutorService executorService;
    private final ClockService clockService;
    private final BotService botService;
    private final PokemonRaidStrategyService strategyService;

    public GymHuntrRaidEventListener(ServerConfigRepository serverConfigRepository, RaidRepository raidRepository,
                                     GymRepository gymRepository, PokemonRepository pokemonRepository,
                                     LocaleService localeService, ExecutorService executorService,
                                     ClockService clockService, BotService botService,
                                     PokemonRaidStrategyService strategyService) {
        this.serverConfigRepository = serverConfigRepository;
        this.raidRepository = raidRepository;
        this.gymRepository = gymRepository;
        this.pokemonRepository = pokemonRepository;
        this.localeService = localeService;
        this.executorService = executorService;
        this.clockService = clockService;
        this.botService = botService;
        this.strategyService = strategyService;
    }

    @Override
    public void onEvent(GenericEvent event) {
        if (event instanceof GuildMessageReceivedEvent) {
            GuildMessageReceivedEvent guildEvent = (GuildMessageReceivedEvent) event;
            final User messageAuthor = guildEvent.getAuthor();
            try {
                if (isUserGymhuntrBot(messageAuthor) || isUserPokeAlarmBot(messageAuthor)) {
                    final String serverName = guildEvent.getGuild().getName().toLowerCase();
                    final Config config = serverConfigRepository.getConfigForServer(serverName);
                    if (config == null) {
                        LOGGER.warn("Server configuration is null for this guild: " + guildEvent.getGuild().getName());
                        return;
                    }

                    if (!config.useBotIntegration()) {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace("Skipping trigger, since bot integration setting is false for server " +
                                    guildEvent.getGuild().getName());
                        }
                        return;
                    }
                    final List<MessageEmbed> embeds = guildEvent.getMessage().getEmbeds();
                    if (embeds != null && embeds.size() > 0) {
                        for (MessageEmbed embed : embeds) {
                            final LocalDateTime currentDateTime = clockService.getCurrentDateTime();
                            final String description = embed.getDescription();
                            final String title = embed.getTitle();
                            List<String> newRaidArguments;
                            if (isUserGymhuntrBot(messageAuthor)) {
                                newRaidArguments = gymhuntrArgumentsToCreateRaid(title, description, clockService);
                            } else if (isUserPokeAlarmBot(messageAuthor)) {
                                newRaidArguments = pokeAlarmArgumentsToCreateRaid(title, description, clockService);
                            } else {
                                newRaidArguments = new ArrayList<>();
                            }
                            try {
                                if (newRaidArguments != null && newRaidArguments.size() > 0) {
                                    final Iterator<String> iterator = newRaidArguments.iterator();
                                    final String gym = iterator.next();
                                    final String pokemon = iterator.next();
                                    final String time = iterator.next();
                                    final Pokemon raidBoss = pokemonRepository.getByName(pokemon);
                                    final String region = config.getRegion();
                                    final Gym raidGym = gymRepository.findByName(gym, region);
                                    final LocalDate currentDate = currentDateTime.toLocalDate();
                                    final LocalDateTime endOfRaid = LocalDateTime.of(currentDate,
                                            LocalTime.parse(time, Utils.timeParseFormatter));
                                    final SelfUser botUser = botService.getBot().getSelfUser();
                                    final PokemonRaidInfo raidInfo;
                                    raidInfo = strategyService.getRaidInfo(raidBoss);
                                    handleRaidFromIntegration(botUser,
                                            guildEvent, raidBoss, raidGym, endOfRaid, config, clockService,
                                            raidInfo, strategyService);
                                } else {
                                    if (LOGGER.isDebugEnabled()) {
                                        LOGGER.debug("No arguments to create raid with for server " + config +
                                                ", skipping. Raw command: " + guildEvent.getMessage().getContentRaw());
                                    }
                                }
                            } catch (Throwable t) {
                                LOGGER.warn("Exception when trying to get arguments for raid creation: " + t.getMessage());
                            }
                        }
                    }
                }
            } catch (Throwable t) {
                LOGGER.warn("Exception thrown for event listener: " + t.getMessage());
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Stacktrace: ", t);
                }
            }
        }
    }

    public void handleRaidFromIntegration(User user, GuildMessageReceivedEvent guildEvent, Pokemon raidBoss, Gym raidGym,
                                          LocalDateTime endOfRaid, Config config, ClockService clockService,
                                          PokemonRaidInfo pokemonRaidInfo,
                                          PokemonRaidStrategyService pokemonRaidStrategyService) {
        Validate.notNull(user, "User");
        Validate.notNull(guildEvent, "Guild event");
        Validate.notNull(config, "Config");
        Validate.notNull(raidBoss, "Raid boss");
        Validate.notNull(raidGym, "Gym");
        Validate.notNull(user, "User");

        final LocalDateTime now = clockService.getCurrentDateTime();
        LocalDateTime currentDateTime = now;
        final boolean moreThan10MinutesLeftOnRaid = endOfRaid.isAfter(currentDateTime.plusMinutes(10));
        if (moreThan10MinutesLeftOnRaid) {
            final Raid raidToCreate = new Raid(raidBoss,
                    endOfRaid,
                    raidGym,
                    localeService, config.getRegion(), false);
            final MessageChannel channel = guildEvent.getChannel();
            try {
                if (raidRepository.isActiveOrExRaidAt(raidGym, config.getRegion())) {
                    Raid existingRaid =
                            raidRepository.getActiveRaidOrFallbackToExRaid(raidGym, config.getRegion(), user);
                    if (existingRaid.getPokemon().isEgg()) {
                        existingRaid = raidRepository.changePokemon(existingRaid, raidBoss,
                                guildEvent.getGuild(), config, user,
                                "(bot) " +
                                        "!raid hatch " + raidBoss.getName() + " " + existingRaid.getGym().getName());
                        LOGGER.info("Hatched raid: " + existingRaid);
                    } else {
                        LOGGER.info("Raid already present, which is not an egg to hatch. " +
                                "Skipping raid at: " + raidGym.getName() + " for server " + config.getServer());
                    }
                } else {
                    createRaid(user, guildEvent, config, clockService, pokemonRaidInfo, now, raidToCreate, channel);
                }
            } catch (Throwable t) {
                LOGGER.warn("Exception when trying to create raid via botintegration for server " +
                        config.getServer() + ", channel " + (channel != null ? channel.getName() : "NULL") + ": " +
                        t.getMessage());
            }
        } else {
            LOGGER.debug("Skipped creating raid at " + raidGym +
                    ", less than 10 minutes remaining on it.");
        }
    }

    protected void createRaid(User user, GuildMessageReceivedEvent guildEvent, Config config,
                              ClockService clockService, PokemonRaidInfo pokemonRaidInfo,
                              LocalDateTime now, Raid raidToCreate, MessageChannel channel) {
        Raid createdRaid;
        if (raidToCreate.isExRaid()) {
            LOGGER.debug("Got an EX raid to create from gym integration, skipping: " + raidToCreate);
            return;
        }
        createdRaid = raidRepository.newRaid(user, raidToCreate, guildEvent.getGuild(), config,
                "(bot) !raid new " + raidToCreate.getPokemon().getName() + " " +
                        printTimeIfSameDay(raidToCreate.getEndOfRaid()) + " " + raidToCreate.getGym().getName());
        final Locale locale = config.getLocale();
        EmbedBuilder embedBuilder = new EmbedBuilder().setTitle(null, null);
        StringBuilder sb = new StringBuilder();
        sb.append(localeService.getMessageFor(LocaleService.NEW_RAID_CREATED,
                locale, createdRaid.toString(locale)));
//        if (user != null && channel != null) {
//            createGroupIfConfigSaysSo(user, guildEvent, config, clockService,
//                    pokemonRaidInfo, now, createdRaid, channel);
//        } else {
//            LOGGER.warn("Could not create group, as some input values were null!");
//        }

        embedBuilder.setDescription(sb.toString());
        final MessageEmbed messageEmbed = embedBuilder.build();
        sendFeedbackThenCleanUp(createdRaid, channel, messageEmbed);
    }

    private void sendFeedbackThenCleanUp(Raid createdRaid, MessageChannel channel, MessageEmbed messageEmbed) {
        LOGGER.info("Raid created via Bot integration for region " + createdRaid.getRegion() + ": " + createdRaid);
        try {
            channel.sendMessage(messageEmbed).queue(m -> {
                // Clean up message
                try {
                    channel.deleteMessageById(m.getId())
                            .queueAfter(BotServerMain.timeToRemoveFeedbackInSeconds, TimeUnit.SECONDS);
                } catch (Throwable t) {
                    LOGGER.warn("Could not clean up feedback from raid creation: " + t.getMessage());
                }
            });
        } catch (Throwable t) {
            LOGGER.debug("Could not send feedback for raid creation: " + t.getMessage());
        }
    }

    private void createGroupIfConfigSaysSo(User user, GuildMessageReceivedEvent guildEvent, Config config,
                                           ClockService clockService, PokemonRaidInfo pokemonRaidInfo,
                                           LocalDateTime now, Raid createdRaid, MessageChannel channel) {
        // Auto create group for tier 5 bosses, if server config says to do so
        if (pokemonRaidInfo != null && pokemonRaidInfo.getBossTier() == 5) {
            LocalTime groupStart = getAutoCreatedRaidGroupStart(now, createdRaid);

            if (groupStart != null) {
                MessageChannel chn = config.getGroupCreationChannel(guildEvent.getGuild());
                MessageChannel channelToCreateGroupIn = channel;
                if (chn != null &&
                        config.getGroupCreationStrategy() == Config.RaidGroupCreationStrategy.NAMED_CHANNEL) {
                    channelToCreateGroupIn = chn;
                }
                if (LOGGER.isDebugEnabled()) {
                    if (channel != null) {
                        LOGGER.debug("Channel to use to create group: " + channel.getName());
                    }
                }
                try {
                    NewRaidGroupCommand.createRaidGroup(channelToCreateGroupIn, guildEvent.getGuild(), config, user,
                            config.getLocale(), groupStart, createdRaid.getId(), localeService, raidRepository,
                            botService, serverConfigRepository, pokemonRepository, gymRepository,
                            clockService, executorService, strategyService);
                } catch (Throwable t) {
                    LOGGER.warn("Could not create raid group for server " + config.getServer() + " and raid " +
                    createdRaid + ": " + t.getMessage());
                }
            }
        } else {
            if (pokemonRaidInfo == null) {
                LOGGER.debug("PokeRaidInfo was null for pokemon " + createdRaid.getPokemon().getName());
            }
        }
    }

    protected static LocalTime getAutoCreatedRaidGroupStart(LocalDateTime now, Raid createdRaid) {
        LocalTime groupStart = null;
        final LocalDateTime endOfRaid = createdRaid.getEndOfRaid();
        final LocalDateTime startOfRaid = getStartOfRaid(endOfRaid, createdRaid.isExRaid());
        final int defaultNumberOfMinutesAfterHatchForGroupCreation = getDefaultNumberOfMinutesAfterHatchForGroupCreation();
        if (now.isBefore(startOfRaid)) {
            groupStart = startOfRaid.toLocalTime().plusMinutes(defaultNumberOfMinutesAfterHatchForGroupCreation);
        } else if (now.isAfter(startOfRaid) && now.plusMinutes(defaultNumberOfMinutesAfterHatchForGroupCreation)
                .plusMinutes(5)
                .isBefore(endOfRaid)) {
            groupStart = now.toLocalTime().plusMinutes(defaultNumberOfMinutesAfterHatchForGroupCreation);
        } else if (now.isBefore(endOfRaid.minusMinutes(10))) {
            groupStart = endOfRaid.toLocalTime().minusMinutes(5);
        }

        return groupStart;
    }

    protected static int getDefaultNumberOfMinutesAfterHatchForGroupCreation() {
        return BotService.currentTier5Bosses.size() > 1 ? 30 : 10;
    }

    public static boolean isUserPokeAlarmBot(User user) {
        return user.isBot() && (user.getName().equalsIgnoreCase("raid") ||
                user.getName().equalsIgnoreCase("egg"));
    }

    public static boolean isUserGymhuntrBot(User user) {
        return user.isBot() && StringUtils.containsIgnoreCase(
                user.getName(), "gymhuntrbot");
    }

    public static List<String> pokeAlarmArgumentsToCreateRaid(String title, String description,
                                                              ClockService clockService) {
        String gym, pokemon, timeString;
        if (title.contains("raid is available against")) {
            final String[] titleSplit = title.replaceAll("!", "").split(" ");
            pokemon = titleSplit[titleSplit.length - 1];
            final String[] descriptionSplitNewLines = description.split("\n");
            final String[] descriptionSplit = descriptionSplitNewLines[0].split(" ");
            timeString = printTime(LocalTime.parse(descriptionSplit[descriptionSplit.length - 3]));
            final String[] gymSplit = title.split("raid is available against");
            gym = gymSplit[0].trim();
        } else if (title.contains("has a level 5") && description.contains("will hatch")) {
            final String[] descriptionSplit = description.split(" ");
            timeString = printTime(LocalTime.parse(descriptionSplit[descriptionSplit.length - 3])
                    .plusMinutes(Utils.RAID_DURATION_IN_MINUTES));
            gym = title.split("has a level 5")[0].trim();
            pokemon = getTier5RaidBossBasedOnSeason(clockService);
        } else if (title.contains("has a level 6") && description.contains("will hatch")) {
            final String[] descriptionSplit = description.split(" ");
            timeString = printTime(LocalTime.parse(descriptionSplit[descriptionSplit.length - 3])
                    .plusMinutes(Utils.RAID_DURATION_IN_MINUTES));
            gym = title.split("has a level 6")[0].trim();
            pokemon = PokemonRepository.EGG_6;
        } else {
            return new ArrayList<>(); // We shouldn't create a raid for this case, non-tier 5 egg
        }
        return Arrays.asList(new String[]{gym, pokemon, timeString});
    }

    protected static String getTier5RaidBossBasedOnSeason(ClockService clockService) {
        String pokemon;
        final List<String> currentTier5Bosses = BotService.currentTier5Bosses;
        if (currentTier5Bosses == null || currentTier5Bosses.size() != 1){
            pokemon = "Egg5";
        } else {
            pokemon = currentTier5Bosses.iterator().next();
        }
        return pokemon;
    }

    public static List<String> gymhuntrArgumentsToCreateRaid(String title, String description,
                                                             ClockService clockService) {
        String gym, pokemon, timeString;
        if (title.contains("Raid has started!")) {
            final String[] firstPass = description.replaceAll("[*]", "")
                    .replaceAll("[.]", "")
                    .replaceAll("Raid Ending: ", "").split("\n");
            final String[] timeArguments = firstPass[3].replaceAll("hours ", "")
                    .replaceAll("min ", "").replaceAll("sec", "").split(" ");
            timeString = printTime(clockService.getCurrentTime()
                    .plusHours(Long.parseLong(timeArguments[0]))
                    .plusMinutes(Long.parseLong(timeArguments[1]))
                    .plusSeconds(Long.parseLong(timeArguments[2])));
            gym = firstPass[0].trim();
            pokemon = firstPass[1].trim();
        } else if (title.contains("Level 5 Raid is starting soon!")) {
            final String[] firstPass = description.replaceAll("[*]", "")
                    .replaceAll("[.]", "")
                    .replaceAll("Raid Starting: ", "").split("\n");
            gym = firstPass[0].trim();
            final String[] timeArguments = firstPass[1].replaceAll("hours ", "")
                    .replaceAll("min ", "").replaceAll("sec", "").split(" ");
            timeString = printTime(clockService.getCurrentTime()
                    .plusHours(Long.parseLong(timeArguments[0]))
                    .plusMinutes(Long.parseLong(timeArguments[1]))
                    .plusSeconds(Long.parseLong(timeArguments[2]))
                    .plusMinutes(Utils.RAID_DURATION_IN_MINUTES));
            pokemon = getTier5RaidBossBasedOnSeason(clockService);
        } else if (title.contains("Level 6 Raid is starting soon!")) {
            final String[] firstPass = description.replaceAll("[*]", "")
                    .replaceAll("[.]", "")
                    .replaceAll("Raid Starting: ", "").split("\n");
            gym = firstPass[0].trim();
            final String[] timeArguments = firstPass[1].replaceAll("hours ", "")
                    .replaceAll("min ", "").replaceAll("sec", "").split(" ");
            timeString = printTime(clockService.getCurrentTime()
                    .plusHours(Long.parseLong(timeArguments[0]))
                    .plusMinutes(Long.parseLong(timeArguments[1]))
                    .plusSeconds(Long.parseLong(timeArguments[2]))
                    .plusMinutes(Utils.RAID_DURATION_IN_MINUTES));
            pokemon = PokemonRepository.EGG_6;
        } else {
            return new ArrayList<>(); // = We shouldn't create this raid, since it is a non-tier 5 egg
        }
        final String[] argumentsInOrder = new String[]{gym, pokemon, timeString};
        return Arrays.asList(argumentsInOrder);
    }
} @Entity
@Table(indexes = @Index(columnList = "server"), name="config")
public class Config {
    @Id
    @Column(nullable = false, unique = true)
    private String id;
    @Column(nullable = false, unique = true)
    private String server;
    @Column(nullable = false)
    private String region;
    @Column(nullable = false)
    private Boolean replyInDmWhenPossible = false;
    @Column(nullable = false)
    private String locale;
    @Column
    private Boolean giveHelp = false;
    @Column
    private Boolean pinGroups = true;
    @Column
    private String overviewMessageId;
    @Column
    private String modPermissionGroup;
    @Column
    private FeedbackStrategy feedbackStrategy = FeedbackStrategy.DEFAULT;
    @Column
    private RaidGroupCreationStrategy groupCreationStrategy = RaidGroupCreationStrategy.SAME_CHANNEL;
    @Column
    private String groupCreationChannel;
    @Column
    private Boolean useBotIntegration = false;

    // For JPA
    protected Config() {
    }

    public Config(String region, Boolean replyInDmWhenPossible, Locale locale, String server) {
        id = UUID.randomUUID().toString();
        Validate.notEmpty(region);
        Validate.notEmpty(server);
        this.region = region.toLowerCase();
        this.replyInDmWhenPossible = replyInDmWhenPossible;
        setLocale(locale);
        this.server = server.toLowerCase();
    }

    public Config(String region, Boolean replyInDmWhenPossible, String server) {
        this(region, replyInDmWhenPossible, LocaleService.DEFAULT, server);
    }

    public Config(String region, String server) {
        this(region, false, server);
    }

    public String getServer() {
        return server;
    }

    public void setServer(String server) {
        this.server = server;
    }

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

    public Boolean getReplyInDmWhenPossible() {
        return replyInDmWhenPossible;
    }

    public void setReplyInDmWhenPossible(Boolean replyInDmWhenPossible) {
        this.replyInDmWhenPossible = replyInDmWhenPossible;
    }

    public Boolean getGiveHelp() {
        return giveHelp;
    }

    public void setGiveHelp(Boolean giveHelp) {
        this.giveHelp = giveHelp;
    }

    public Boolean isPinGroups() {
        return pinGroups;
    }

    public void setPinGroups(Boolean pinGroups) {
        this.pinGroups = pinGroups;
    }

    public Boolean useBotIntegration() {
        if (useBotIntegration != null) {
            return useBotIntegration;
        } else {
            return false;
        }
    }

    public void setUseBotIntegration(Boolean useBotIntegration) {
        this.useBotIntegration = useBotIntegration;
    }

    public Locale getLocale() {
        if (locale != null) {
            return new Locale(locale);
        } else {
            return LocaleService.DEFAULT;
        }
    }

    public void setLocale(Locale locale) {
        Validate.notNull(locale);
        this.locale = locale.getLanguage();
    }

    public String getId() {
        return id;
    }

    public void setOverviewMessageId(String overviewMessageId) {
        this.overviewMessageId = overviewMessageId;
    }

    public String getOverviewMessageId() {
        return overviewMessageId;
    }

    public String getModPermissionGroup() {
        return modPermissionGroup;
    }

    public void setModPermissionGroup(String modPermissionGroup) {
        this.modPermissionGroup = modPermissionGroup;
    }

    public FeedbackStrategy getFeedbackStrategy() {
        return feedbackStrategy == null ? FeedbackStrategy.DEFAULT : feedbackStrategy;
    }

    public void setFeedbackStrategy(FeedbackStrategy feedbackStrategy) {
        this.feedbackStrategy = feedbackStrategy;
    }

    public RaidGroupCreationStrategy getGroupCreationStrategy() {
        return groupCreationStrategy;
    }

    public void setGroupCreationStrategy(RaidGroupCreationStrategy groupCreationStrategy) {
        this.groupCreationStrategy = groupCreationStrategy;
    }

    public String getGroupCreationChannel() {
        return groupCreationChannel;
    }

    public void setGroupCreationChannel(String groupCreationChannel) {
        this.groupCreationChannel = groupCreationChannel;
    }

    public MessageChannel getGroupCreationChannel(Guild guild) {
        if (guild == null) {
            return null;
        }
        for (MessageChannel c : guild.getTextChannels()) {
            if (c.getName().equalsIgnoreCase(groupCreationChannel)) {
                return c;
            }
        }
        return null;
    }

    public enum FeedbackStrategy {
        DEFAULT, KEEP_ALL, REMOVE_ALL_EXCEPT_MAP // This actually also removes maps, but after some time (after wishes from users)
    }

    public enum RaidGroupCreationStrategy {
        SAME_CHANNEL, NAMED_CHANNEL // Create group in named group, not necessarily the channel where command originated
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Config)) return false;

        Config config = (Config) o;

        if (id != null ? !id.equals(config.id) : config.id != null) return false;
        if (server != null ? !server.equals(config.server) : config.server != null) return false;
        if (region != null ? !region.equals(config.region) : config.region != null) return false;
        if (replyInDmWhenPossible != null ? !replyInDmWhenPossible.equals(config.replyInDmWhenPossible) : config.replyInDmWhenPossible != null)
            return false;
        if (locale != null ? !locale.equals(config.locale) : config.locale != null) return false;
        if (giveHelp != null ? !giveHelp.equals(config.giveHelp) : config.giveHelp != null) return false;
        if (pinGroups != null ? !pinGroups.equals(config.pinGroups) : config.pinGroups != null) return false;
        if (overviewMessageId != null ? !overviewMessageId.equals(config.overviewMessageId) : config.overviewMessageId != null)
            return false;
        if (modPermissionGroup != null ? !modPermissionGroup.equals(config.modPermissionGroup) : config.modPermissionGroup != null)
            return false;
        if (feedbackStrategy != config.feedbackStrategy) return false;
        if (groupCreationStrategy != config.groupCreationStrategy) return false;
        if (groupCreationChannel != null ? !groupCreationChannel.equals(config.groupCreationChannel) : config.groupCreationChannel != null)
            return false;
        return useBotIntegration != null ? useBotIntegration.equals(config.useBotIntegration) : config.useBotIntegration == null;
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (server != null ? server.hashCode() : 0);
        result = 31 * result + (region != null ? region.hashCode() : 0);
        result = 31 * result + (replyInDmWhenPossible != null ? replyInDmWhenPossible.hashCode() : 0);
        result = 31 * result + (locale != null ? locale.hashCode() : 0);
        result = 31 * result + (giveHelp != null ? giveHelp.hashCode() : 0);
        result = 31 * result + (pinGroups != null ? pinGroups.hashCode() : 0);
        result = 31 * result + (overviewMessageId != null ? overviewMessageId.hashCode() : 0);
        result = 31 * result + (modPermissionGroup != null ? modPermissionGroup.hashCode() : 0);
        result = 31 * result + (feedbackStrategy != null ? feedbackStrategy.hashCode() : 0);
        result = 31 * result + (groupCreationStrategy != null ? groupCreationStrategy.hashCode() : 0);
        result = 31 * result + (groupCreationChannel != null ? groupCreationChannel.hashCode() : 0);
        result = 31 * result + (useBotIntegration != null ? useBotIntegration.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Config{" +
                "server='" + server + '\'' +
                ", region='" + region + '\'' +
                ", replyInDm=" + replyInDmWhenPossible +
                ", locale='" + locale + '\'' +
                ", giveHelp=" + giveHelp +
                ", pinGroups=" + pinGroups +
                ", overview='" + overviewMessageId + '\'' +
                ", modGroup='" + modPermissionGroup + '\'' +
                ", feedback=" + feedbackStrategy +
                ", groupCreationStrategy=" + groupCreationStrategy +
                ", groupCreationChannel='" + groupCreationChannel + '\'' +
                ", useBotIntegration=" + useBotIntegration +
                '}';
    }
} @Transactional(propagation = Propagation.REQUIRES_NEW)
public interface ServerConfigRepository extends JpaRepository<Config, String> {
    Config findByServer(String server);

    default Config getConfigForServer(String server) {
        final Config config = findByServer(server);
        return config;
    }

    default Map<String, Config> getAllConfig() {
        Map<String, Config> configs = new HashMap<>();
        for (Config config : findAll()) {
            configs.put(config.getServer(), config);
        }
        return configs;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    default void setOverviewMessageIdForServer(String server, String overviewMessageId) {
        final Config config = findByServer(server);
        config.setOverviewMessageId(overviewMessageId);
        save(config);
    }
} @Transactional(propagation = Propagation.REQUIRES_NEW)
public interface UserConfigRepository extends JpaRepository<UserConfig, String> {
} public class AggregateCommandListener implements CommandListener {
    private LinkedList<CommandListener> listeners = new LinkedList<>();

    public AggregateCommandListener(Collection<CommandListener> listeners) {
        this.listeners.addAll(listeners);
    }

    @Override
    public void onCommand(CommandEvent event, Command command) {
        for (CommandListener listener : listeners) {
            listener.onCommand(event, command);
        }
    }

    @Override
    public void onCompletedCommand(CommandEvent event, Command command) {
        for (CommandListener listener : listeners) {
            listener.onCompletedCommand(event, command);
        }
    }

    @Override
    public void onTerminatedCommand(CommandEvent event, Command command) {
        for (CommandListener listener : listeners) {
            listener.onTerminatedCommand(event, command);
        }
    }

    @Override
    public void onNonCommandMessage(MessageReceivedEvent event) {
        for (CommandListener listener : listeners) {
            listener.onNonCommandMessage(event);
        }
    }
} public class SignupWithPlusCommandListener implements EventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(SignupWithPlusCommandListener.class);

    private final RaidRepository raidRepository;
    private final PokemonRepository pokemonRepository;
    private final ServerConfigRepository serverConfigRepository;
    private final BotService botService;
    private final LocaleService localeService;

    public SignupWithPlusCommandListener(RaidRepository raidRepository, PokemonRepository pokemonRepository,
                                         ServerConfigRepository serverConfigRepository, BotService botService, LocaleService localeService) {
        this.raidRepository = raidRepository;
        this.pokemonRepository = pokemonRepository;
        this.serverConfigRepository = serverConfigRepository;
        this.botService = botService;
        this.localeService = localeService;
    }

    public static final String plusXRegExp = "^[+]\\d{1,2}\\s{1,2}\\d{1,2}[:.]?\\d{2}\\s{1,2}.*";
    @Override
    public void onEvent(GenericEvent event) {
        if (event instanceof GuildMessageReceivedEvent) {
            final GuildMessageReceivedEvent guildMessageReceivedEvent = (GuildMessageReceivedEvent) event;
            if (guildMessageReceivedEvent.getAuthor().isBot()) {
                return;
            }

            final String rawContent = guildMessageReceivedEvent.getMessage().getContentRaw();
            if (rawContent.matches(plusXRegExp)) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("It would seem this is a + command to add signups: " + rawContent);
                }
                String strippedContent = rawContent.replaceAll("\\s{2,4}", " ")
                        .replaceAll("[+]", "");
                String[] splitArguments = strippedContent.split("\\s{1}");
                attemptSignUpFromPlusCommand(guildMessageReceivedEvent, splitArguments);
            }
        }
    }

    private void attemptSignUpFromPlusCommand(GuildMessageReceivedEvent guildMessageReceivedEvent, String[] splitArguments) {
        final String numberOfPeopleArgument = splitArguments[0];
        final String etaArgument = splitArguments[1];
        final String[] gymArgument = ArrayUtils.removeAll(splitArguments, 0, 1);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Trying to add " + numberOfPeopleArgument + " to raid, ETA " + etaArgument + " to gym " +
                    StringUtils.join(gymArgument, " "));
        }
        final String guild = guildMessageReceivedEvent.getGuild().getName().trim().toLowerCase();
        final Config configForServer = serverConfigRepository.getConfigForServer(guild);
        final User user = guildMessageReceivedEvent.getAuthor();
        String message;
        try {
            message = raidRepository.executeSignUpCommand(configForServer, user,
                    localeService.getLocaleForUser(user),
                    splitArguments, "signup");
            guildMessageReceivedEvent.getMessage().addReaction(Emotes.OK).queue();
            ConfigAwareCommand.removeOriginMessageIfConfigSaysSo(configForServer, guildMessageReceivedEvent);
        } catch (Throwable t) {
            LOGGER.debug("Signup plus command failed: " + t.getMessage());
            message = t.getMessage() + "\n\n" +
            "Syntax: *+1 09:45 Solna Platform*";
            guildMessageReceivedEvent.getMessage().addReaction(Emotes.ERROR).queue();
            ConfigAwareCommand.removeOriginMessageIfConfigSaysSo(configForServer, guildMessageReceivedEvent);
        }
        if (!StringUtils.isEmpty(message)) {
            EmbedBuilder embedBuilder = new EmbedBuilder();
            embedBuilder.setAuthor(null, null, null);
            embedBuilder.setTitle(null);
            embedBuilder.setDescription(message);
            final String msgRemoveText =
                    localeService.getMessageFor(LocaleService.KEEP_CHAT_CLEAN,
                            localeService.getLocaleForUser(user), "15");
            embedBuilder.setFooter(msgRemoveText, null);
            guildMessageReceivedEvent.getMessage().getChannel().sendMessage(embedBuilder.build())
                    .queue(msg -> {
                        msg.delete().queueAfter(15, TimeUnit.SECONDS); // Clean up feedback after x seconds
                    }
            );
            LOGGER.debug("Added signup.");
        }
    }
} public class StartUpEventListener implements EventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(StartUpEventListener.class);
    private ServerConfigRepository serverConfigRepository;
    private final RaidRepository raidRepository;
    private final LocaleService localeService;
    private final ClockService clockService;
    private final ExecutorService executorService;
    private final BotService botService;
    private final GymRepository gymRepository;
    private final PokemonRepository pokemonRepository;
    private final PokemonRaidStrategyService pokemonRaidStrategyService;

    public StartUpEventListener(ServerConfigRepository serverConfigRepository,
                                RaidRepository raidRepository, LocaleService localeService,
                                ClockService clockService, ExecutorService executorService, BotService botService,
                                GymRepository gymRepository, PokemonRepository pokemonRepository,
                                PokemonRaidStrategyService pokemonRaidStrategyService) {
        this.serverConfigRepository = serverConfigRepository;
        this.raidRepository = raidRepository;
        this.localeService = localeService;
        this.clockService = clockService;
        this.executorService = executorService;
        this.botService = botService;
        this.gymRepository = gymRepository;
        this.pokemonRepository = pokemonRepository;
        this.pokemonRaidStrategyService = pokemonRaidStrategyService;
    }

    @Override
    public void onEvent(GenericEvent event) {
        if (event instanceof ReadyEvent) {
            LOGGER.info("ReadyEvent received, connecting to server overviews ..");
            final List<Guild> guilds = event.getJDA().getGuilds();
            executorService.execute(() -> {
                for (Guild guild : guilds) {
                    Config config = serverConfigRepository.getConfigForServer(guild.getName().trim().toLowerCase());
                    if (config != null) {
                        final String messageId = config.getOverviewMessageId();
                        LOGGER.info("Trying to connect to overview for " + guild.getName() + ": " + messageId);
                        if (!StringUtils.isEmpty(messageId)) {
                            for (MessageChannel channel : guild.getTextChannels()) {
                                if (attachToOverviewMessageIfExists(guild, config, messageId, channel,
                                        pokemonRaidStrategyService)) {
                                    break;
                                } else {
                                    if (LOGGER.isDebugEnabled()) {
                                        LOGGER.debug("Didn't find overview in channel " + channel.getName());
                                    }
                                }
                            }
                        }

                        final List<RaidGroup> groupsForServer = raidRepository.getGroupsForServer(config.getServer());
                        for (RaidGroup group : groupsForServer) {
                            attachToGroupMessage(guild, config, group);
                        }
                    }
                }
            });
        }
    }

    private boolean attachToGroupMessage(Guild guild, Config config,
                                         RaidGroup raidGroup) {
        MessageChannel channel = null;
        try {
            final List<TextChannel> textChannels = guild.getTextChannels();
            for (TextChannel textChannel : textChannels) {
                if (textChannel.getName().equalsIgnoreCase(raidGroup.getChannel())) {
                    channel = textChannel;
                    break;
                }
            }
            final Locale locale = config.getLocale();
            Raid raid = raidRepository.getById(raidGroup.getRaidId());
            final EmoticonSignUpMessageListener emoticonSignUpMessageListener =
                    new EmoticonSignUpMessageListener(botService, localeService, serverConfigRepository,
                            raidRepository, pokemonRepository, gymRepository, raid.getId(), raidGroup.getStartsAt(),
                            raidGroup.getCreatorId());
            emoticonSignUpMessageListener.setEmoteMessageId(raidGroup.getEmoteMessageId());
            emoticonSignUpMessageListener.setInfoMessageId(raidGroup.getInfoMessageId());
            final int delayTime = raid.isExRaid() ? 1 : 15;
            final TimeUnit delayTimeUnit = raid.isExRaid() ? TimeUnit.MINUTES : TimeUnit.SECONDS;
            final Callable<Boolean> groupEditTask =
                    NewRaidGroupCommand.getMessageRefreshingTaskToSchedule(channel, raid,
                            emoticonSignUpMessageListener,
                            raidGroup.getInfoMessageId(), locale, raidRepository,
                            localeService,
                            clockService, executorService, botService, delayTimeUnit, delayTime, raidGroup.getId());
            executorService.submit(groupEditTask);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Found group message for raid " + raid + " in channel " +
                        (channel == null ? "N/A" : channel.getName()) +
                        " (server " + guild.getName() + "). Attaching to it.");
            }
            return true;
        } catch (UserMessedUpException e) {
            if (channel != null)
                channel.sendMessage(e.getMessage()).queue(m -> {
                    m.delete().queueAfter(BotServerMain.timeToRemoveFeedbackInSeconds, TimeUnit.SECONDS);
                });
        } catch (ErrorResponseException e) {
            // We couldn't find the message in this channel or had permission issues, ignore
            LOGGER.info("Caught exception during startup: " + e.getMessage());
            LOGGER.warn("Cleaning up raidgroup...");
            cleanUpRaidGroup(raidGroup);
            LOGGER.debug("Stacktrace:", e);
        } catch (Throwable e) {
            LOGGER.warn("Cleaning up raidgroup due to exception: " + e.getMessage());
            cleanUpRaidGroup(raidGroup);
        }
        return false;
    }

    private void cleanUpRaidGroup(RaidGroup raidGroup) {
        try {
            RaidGroup deletedRaidGroup = raidRepository.deleteGroupInNewTransaction(raidGroup.getRaidId(), raidGroup.getId());
            if (deletedRaidGroup != null) {
                LOGGER.debug("Cleaned up raid group: " + deletedRaidGroup);
            } else {
                LOGGER.debug("Didn't delete raid group, it was apparantly deleted already.");
            }
        } catch (Throwable t) {
            // Ignore any other error and try the other server channels
            LOGGER.warn("Exception when cleaning up group " + raidGroup + ": " + t.getMessage());
        }
    }

    private boolean attachToOverviewMessageIfExists(Guild guild, Config config, String messageId,
                                                    MessageChannel channel, PokemonRaidStrategyService raidStrategyService) {
        try {
            if (channel.retrieveMessageById(messageId).complete() != null) {
                final Locale locale = config.getLocale();
                final Callable<Boolean> overviewTask =
                        RaidOverviewCommand.getMessageRefreshingTaskToSchedule(
                                null, config.getServer(), messageId, localeService, locale, serverConfigRepository,
                                raidRepository, clockService, channel,
                                executorService, raidStrategyService);
                executorService.submit(overviewTask);
                LOGGER.info("Found overview message for channel " + channel.getName() +
                        " (server " + guild.getName() + "). Attaching to it.");
                return true;
            }
        } catch (UserMessedUpException e) {
            LOGGER.warn("Could not attach to message due to an error: " + e.getMessage());
        } catch (ErrorResponseException e) {
            // We couldn't find the message in this channel or had permission issues, ignore
        } catch (Throwable e) {
            // Ignore any other error and try the other server channels
        }
        return false;
    }
} public class UnsignWithMinusCommandListener implements EventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(UnsignWithMinusCommandListener.class);

    private final RaidRepository raidRepository;
    private final PokemonRepository pokemonRepository;
    private final ServerConfigRepository serverConfigRepository;
    private final BotService botService;
    private final LocaleService localeService;

    public UnsignWithMinusCommandListener(RaidRepository raidRepository, PokemonRepository pokemonRepository,
                                          ServerConfigRepository serverConfigRepository, BotService botService,
                                          LocaleService localeService) {
        this.raidRepository = raidRepository;
        this.pokemonRepository = pokemonRepository;
        this.serverConfigRepository = serverConfigRepository;
        this.botService = botService;
        this.localeService = localeService;
    }

    public static final String minusXRegExp = "^[-]\\d{1,2}\\s{1,2}.*";
    @Override
    public void onEvent(GenericEvent event) {
        if (event instanceof GuildMessageReceivedEvent) {
            final GuildMessageReceivedEvent guildMessageReceivedEvent = (GuildMessageReceivedEvent) event;
            if (guildMessageReceivedEvent.getAuthor().isBot()) {
                return;
            }

            final String rawContent = guildMessageReceivedEvent.getMessage().getContentRaw();
            if (rawContent.matches(minusXRegExp)) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("It would seem this is a - command to remove signups: " + rawContent);
                }
                String strippedContent = rawContent.replaceAll("\\s{2,4}", " ")
                        .replaceFirst("[-]", "");
                String[] splitArguments = strippedContent.split("\\s{1}");
                attemptUnsignFromMinusCommand(guildMessageReceivedEvent, splitArguments);
            }
        }
    }

    private void attemptUnsignFromMinusCommand(GuildMessageReceivedEvent guildMessageReceivedEvent, String[] splitArguments) {
        final String numberOfPeopleArgument = splitArguments[0];
        final String[] gymArgument = ArrayUtils.removeAll(splitArguments, 0);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Trying to remove " + numberOfPeopleArgument + " from raid, gym " +
                    StringUtils.join(gymArgument, " "));
        }
        final String guild = guildMessageReceivedEvent.getGuild().getName().trim().toLowerCase();
        final Config configForServer = serverConfigRepository.getConfigForServer(guild);
        final User user = guildMessageReceivedEvent.getAuthor();
        String message;
        try {
            message = raidRepository.executeUnsignCommand(configForServer, user,
                    localeService.getLocaleForUser(user),
                    splitArguments, "signup");
            guildMessageReceivedEvent.getMessage().addReaction(Emotes.OK).queue();
            ConfigAwareCommand.removeOriginMessageIfConfigSaysSo(configForServer, guildMessageReceivedEvent);
        } catch (Throwable t) {
            LOGGER.debug("Unsign command failed: " + t.getMessage());
            message = t.getMessage() + "\n\n" +
            "Syntax: *-1 Solna Platform*";
            guildMessageReceivedEvent.getMessage().addReaction(Emotes.ERROR).queue();
            ConfigAwareCommand.removeOriginMessageIfConfigSaysSo(configForServer, guildMessageReceivedEvent);
        }
        if (!StringUtils.isEmpty(message)) {
            EmbedBuilder embedBuilder = new EmbedBuilder();
            embedBuilder.setAuthor(null, null, null);
            embedBuilder.setTitle(null);
            embedBuilder.setDescription(message);
            final String msgRemoveText =
                    localeService.getMessageFor(LocaleService.KEEP_CHAT_CLEAN,
                            localeService.getLocaleForUser(user),
                            "15");
            embedBuilder.setFooter(msgRemoveText, null);
            guildMessageReceivedEvent.getMessage().getChannel().sendMessage(embedBuilder.build())
                    .queue(msg -> {
                        msg.delete().queueAfter(15, TimeUnit.SECONDS); // Clean up feedback after x seconds
                    }
            );
            LOGGER.debug("Removed signup.");
        }
    }
}
import com.jagrosh.jdautilities.command.CommandClient;
import com.jagrosh.jdautilities.command.CommandClientBuilder;
import com.jagrosh.jdautilities.command.CommandListener;
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
import com.jagrosh.jdautilities.examples.command.AboutCommand;
import com.jagrosh.jdautilities.examples.command.PingCommand;
import net.dv8tion.jda.api.*;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.hooks.EventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import pokeraidbot.commands.*;
import pokeraidbot.domain.config.ClockService;
import pokeraidbot.domain.config.LocaleService;
import pokeraidbot.domain.emote.Emotes;
import pokeraidbot.domain.gym.GymRepository;
import pokeraidbot.domain.pokemon.PokemonRepository;
import pokeraidbot.domain.raid.PokemonRaidStrategyService;
import pokeraidbot.domain.raid.RaidRepository;
import pokeraidbot.domain.raid.signup.EmoticonSignUpMessageListener;
import pokeraidbot.domain.tracking.TrackingService;
import pokeraidbot.infrastructure.botsupport.gymhuntr.GymHuntrRaidEventListener;
import pokeraidbot.infrastructure.jpa.config.Config;
import pokeraidbot.infrastructure.jpa.config.ServerConfigRepository;
import pokeraidbot.infrastructure.jpa.config.UserConfigRepository;
import pokeraidbot.jda.AggregateCommandListener;
import pokeraidbot.jda.SignupWithPlusCommandListener;
import pokeraidbot.jda.StartUpEventListener;
import pokeraidbot.jda.UnsignWithMinusCommandListener;
import javax.security.auth.login.LoginException;
import java.awt.*;
import java.time.LocalDate;
import java.time.Month;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
package pokeraidbot;



public class BotService {
    private static final Logger LOGGER = LoggerFactory.getLogger(BotService.class);
    private final Set<EventListener> extraListeners = new CopyOnWriteArraySet<>();
    private TrackingService trackingService;
    private String ownerId;
    private String token;
    private JDA botInstance;
    private CommandClient commandClient;
    private CommandListener aggregateCommandListener;
    private GymRepository gymRepository;
    private ServerConfigRepository serverConfigRepository;
    private UserConfigRepository userConfigRepository;