@SpringBootApplication
@Configuration
@EnableAutoConfiguration
@EntityScan(basePackages = "pokeraidbot.infrastructure.jpa")
@EnableJpaRepositories(basePackages = "pokeraidbot.infrastructure.jpa")
@ComponentScan(basePackages = {"pokeraidbot"})
@EnableTransactionManagement
public class BotServerMain {
    public static String BOT_CREATOR_USERID = "199969842021793792";
    @Value("${ownerId}")
    private String ownerId;
    @Value("${token}")
    private String token;

    public static final String version = "1.9.0"; // todo: should be filter copied from pom.xml
    public static final int timeToRemoveFeedbackInSeconds = 30; // todo: should be setting?

    public static void main(String[] args) throws InterruptedException, IOException, LoginException, RateLimitedException {
        SpringApplication.run(BotServerMain.class, args);
    }

    @Bean
    public LocaleService getLocaleService(UserConfigRepository userConfigRepository, @Value("${locale:sv}")String locale) {
        return new LocaleService(locale, userConfigRepository);
    }

    @Bean
    public ClockService getClockService() {
        final ClockService clockService = new ClockService();
        // If you want to test, and it's currently in the "dead time" where raids can't be created, set time manually like this
        //clockService.setMockTime(LocalTime.of(9, 0));
        Utils.setClockService(clockService);
        return clockService;
    }

    @Bean
    public TrackingService getTrackingService(LocaleService localeService,
                                              UserConfigRepository userConfigRepository,
                                              PokemonRepository pokemonRepository) {
        return new TrackingService(localeService, userConfigRepository,
                pokemonRepository);
    }

    @Bean
    public BotService getBotService(LocaleService localeService, GymRepository gymRepository, RaidRepository raidRepository,
                                    PokemonRepository pokemonRepository, PokemonRaidStrategyService raidInfoService,
                                    ServerConfigRepository serverConfigRepository,
                                    UserConfigRepository userConfigRepository, ClockService clockService,
                                    ExecutorService executorService, TrackingService trackingService) {
        return new BotService(localeService, gymRepository, raidRepository, pokemonRepository, raidInfoService,
                serverConfigRepository, userConfigRepository, executorService, clockService, trackingService, ownerId, token);
    }

    @Bean
    public ExecutorService getExecutorService() {
        return new ThreadPoolExecutor(100, Integer.MAX_VALUE,
                65L, TimeUnit.SECONDS,
                new LinkedTransferQueue<>());
    }

    @Bean
    public GymRepository getGymRepository(LocaleService localeService, ServerConfigRepository serverConfigRepository) {
        return new GymRepository(serverConfigRepository, localeService);
    }

    @Bean
    public RaidRepository getRaidRepository(LocaleService localeService, RaidEntityRepository entityRepository,
                                            PokemonRepository pokemonRepository, GymRepository gymRepository,
                                            ClockService clockService, TrackingService trackingService) {
        return new RaidRepository(clockService, localeService, entityRepository, pokemonRepository, gymRepository,
                trackingService);
    }

    @Bean
    public PokemonRepository getPokemonRepository(LocaleService localeService) {
        return new PokemonRepository("/pokemons.csv", localeService);
    }

    @Bean
    public PokemonRaidStrategyService getRaidInfoService(PokemonRepository pokemonRepository) {
        return new PokemonRaidStrategyService(pokemonRepository);
    }
} public class Utils {
    public static final DateTimeFormatter timeParseFormatter = DateTimeFormatter.ofPattern("H[:][.]mm");
    public static final DateTimeFormatter dateAndTimeParseFormatter =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH[:][.]mm");
    public static final DateTimeFormatter timePrintFormatter = DateTimeFormatter.ofPattern("HH:mm");
    public static final DateTimeFormatter dateAndTimePrintFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
    public static final int HIGH_LIMIT_FOR_SIGNUPS = 20;
    public static final int RAID_DURATION_IN_MINUTES = 45;
    public static final int EX_RAID_DURATION_IN_MINUTES = 45;
    private static final String EX_RAID_BOSS = "deoxys";
    private static ClockService clockService = new ClockService();
    private static ResistanceTable resistanceTable = new ResistanceTable();

    public static ClockService getClockService() {
        return clockService;
    }

    public static void setClockService(ClockService clockService) {
        Utils.clockService = clockService;
    }

    public static String printDateTime(LocalDateTime dateTime) {
        return dateTime.format(dateAndTimePrintFormatter);
    }

    public static String printTime(LocalTime time) {
        return time.format(timePrintFormatter);
    }

    public static String printTimeIfSameDay(LocalDateTime dateAndTime) {
        if (dateAndTime.toLocalDate().isEqual(LocalDate.now())) {
            return dateAndTime.toLocalTime().format(timePrintFormatter);
        } else {
            return printDateTime(dateAndTime);
        }
    }

    public static boolean isTypeDoubleStrongVsPokemonWithTwoTypes(String pokemonTypeOne,
                                                                  String pokemonTypeTwo, String typeToCheck) {
        Validate.notEmpty(pokemonTypeOne);
        Validate.notEmpty(pokemonTypeTwo);
        Validate.notEmpty(typeToCheck);
        if (typeIsStrongVsPokemon(pokemonTypeOne, typeToCheck) && typeIsStrongVsPokemon(pokemonTypeTwo, typeToCheck)) {
            return true;
        } else {
            return false;
        }
    }

    public static Set<String> getWeaknessesFor(PokemonTypes pokemonType) {
        return resistanceTable.getWeaknesses(pokemonType);
    }

    public static boolean typeIsStrongVsPokemon(String pokemonType, String typeToCheck) {
        return resistanceTable.typeIsStrongVsPokemon(pokemonType, typeToCheck);
    }

    public static void assertSignupTimeNotBeforeRaidStartAndNow(User user, LocalDateTime dateAndTime,
                                                                LocalDateTime endOfRaid, LocaleService localeService,
                                                                boolean isExRaid) {
        final LocalDateTime startOfRaid = getStartOfRaid(endOfRaid, isExRaid);
        final LocalDateTime now = clockService.getCurrentDateTime();
        assertSignupTimeNotBeforeRaidStart(user, dateAndTime, endOfRaid, localeService, isExRaid);
        if (dateAndTime.isBefore(now)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.SIGN_BEFORE_NOW, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(now)));
        }
    }

    public static LocalDateTime getStartOfRaid(LocalDateTime endOfRaid, boolean isExRaid) {
        return isExRaid ? endOfRaid.minusMinutes(EX_RAID_DURATION_IN_MINUTES) :
                endOfRaid.minusMinutes(RAID_DURATION_IN_MINUTES);
    }

    public static void assertSignupTimeNotBeforeRaidStart(User user, LocalDateTime dateAndTime,
                                                          LocalDateTime endOfRaid, LocaleService localeService,
                                                          boolean isExRaid) {
        final LocalDateTime startOfRaid = getStartOfRaid(endOfRaid, isExRaid);
        if (dateAndTime.isBefore(startOfRaid)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.SIGN_BEFORE_RAID, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(startOfRaid)));
        }
    }

    public static void assertGroupTimeNotBeforeNow(User user, LocalDateTime dateAndTime,
                                                        LocaleService localeService) {
        final LocalDateTime now = clockService.getCurrentDateTime();
        if (dateAndTime.isBefore(now)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_GROUP_BEFORE_NOW, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(now)));
        }
    }

    public static void assertCreateRaidTimeNotBeforeNow(User user, LocalDateTime dateAndTime,
                                                        LocaleService localeService) {
        final LocalDateTime now = clockService.getCurrentDateTime();
        if (dateAndTime.isBefore(now)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.TIMEZONE, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(now)));
        }
    }

    public static void assertTimeNotInNoRaidTimespan(User user, LocalTime time, LocaleService localeService) {
        if (time.isAfter(LocalTime.of(23, 0)) || time.isBefore(LocalTime.of(5, 0))) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAIDS_NOW, localeService.getLocaleForUser(user),
                            printTime(time)));
        }
    }

    public static void assertTimeNotMoreThanXHoursFromNow(User user, LocalTime time,
                                                          LocaleService localeService, Integer hours) {
        final LocalTime now = clockService.getCurrentTime();
        if (now.isBefore(LocalTime.of(22, 0)) && now.plusHours(2).isBefore(time)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAID_TOO_LONG, localeService.getLocaleForUser(user),
                            printTime(time), printTime(now), String.valueOf(hours)));
        }
    }

    public static void assertEtaNotAfterRaidEnd(User user, Raid raid, LocalDateTime eta, LocaleService localeService) {
        if (eta.isAfter(raid.getEndOfRaid())) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_ETA_AFTER_RAID,
                            localeService.getLocaleForUser(user), printTimeIfSameDay(eta),
                            printTimeIfSameDay(raid.getEndOfRaid())));
        }
    }

    public static String getStaticMapUrl(Gym gym) {
        // todo: host marker png via pokeraidbot web
        String url = "https://maps.googleapis.com/maps/api/staticmap?center=" + gym.getX() + "," + gym.getY() +
                "&zoom=14&size=400x400&maptype=roadmap&markers=icon:http://millert.se/pogo/marker_xsmall.png%7C" +
                gym.getX() + "," + gym.getY() + "&key=AIzaSyAZm7JLojr2KaUvkeHEpHh0Y-zPwP3dpCU";
        return url;
    }

    public static String getNonStaticMapUrl(Gym gym) {
        String url = "http://www.google.com/maps?q=" + gym.getX() + "," + gym.getY();
        return url;
    }

    public static String printWeaknesses(Pokemon pokemon) {
        Set<String> weaknessesToPrint = new LinkedHashSet<>();
        final Set<String> typeSet = pokemon.getTypes().getTypeSet();
        for (String weakness : pokemon.getWeaknesses()) {
            if (typeSet.size() > 1) {
                final Iterator<String> iterator = typeSet.iterator();
                if (isTypeDoubleStrongVsPokemonWithTwoTypes(iterator.next(), iterator.next(), weakness)) {
                    weaknessesToPrint.add("**" + weakness + "**");
                } else {
                    weaknessesToPrint.add(weakness);
                }
            } else {
                weaknessesToPrint.add(weakness);
            }
        }

        return StringUtils.join(weaknessesToPrint, ", ");
    }

    public static boolean isSamePokemon(String pokemonName, String existingEntityPokemon) {
        return pokemonName.equalsIgnoreCase(existingEntityPokemon);
    }

    public static boolean raidsCollide(LocalDateTime endOfRaid, boolean isExRaid, LocalDateTime endOfRaidTwo,
                                       boolean isExRaidTwo) {
        LocalDateTime startTime = getStartOfRaid(endOfRaid, isExRaid);
        LocalDateTime startTimeTwo = getStartOfRaid(endOfRaidTwo, isExRaidTwo);
        return isInInterval(startTime, endOfRaid, startTimeTwo, endOfRaidTwo) ||
                isInInterval(startTimeTwo, endOfRaidTwo, startTime, endOfRaid);
    }

    private static boolean isInInterval(LocalDateTime startTime, LocalDateTime endOfRaid,
                                        LocalDateTime startTimeTwo, LocalDateTime endOfRaidTwo) {
        return (startTime.isAfter(startTimeTwo) && startTime.isBefore(endOfRaidTwo)) ||
                (endOfRaid.isBefore(endOfRaidTwo) && endOfRaid.isAfter(startTimeTwo));
    }

    public static boolean isRaidExPokemon(String pokemonName, PokemonRaidStrategyService strategyService, PokemonRepository pokemonRepository) {
        Pokemon pokemon = pokemonRepository.getByName(pokemonName);
        return strategyService.getRaidInfo(pokemon).getBossTier() == 5;
    }

    public static LocalTime parseTime(User user, String timeString, LocaleService localeService) {
        LocalTime endsAtTime;
        try {
            timeString = preProcessTimeString(timeString);
            endsAtTime = LocalTime.parse(timeString, Utils.timeParseFormatter);
        } catch (DateTimeParseException | NullPointerException e) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.BAD_DATETIME_FORMAT, localeService.getLocaleForUser(user),
                            "HH:MM", timeString));
        }
        return endsAtTime;
    }

    public static Integer assertNotTooManyOrNoNumber(User user, LocaleService localeService, String people) {
        Integer numberOfPeople;
        try {
            numberOfPeople = new Integer(people);
            if (numberOfPeople < 1 || numberOfPeople > HIGH_LIMIT_FOR_SIGNUPS) {
                throw new RuntimeException();
            }
        } catch (RuntimeException e) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.ERROR_PARSE_PLAYERS,
                            localeService.getLocaleForUser(user),
                            people, String.valueOf(HIGH_LIMIT_FOR_SIGNUPS)));
        }
        return numberOfPeople;
    }

    public static LocalDate parseDate(User user, String dateString, LocaleService localeService) {
        LocalDate theDate;
        try {
            theDate = LocalDate.parse(dateString);
        } catch (DateTimeException | NullPointerException e) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.BAD_DATETIME_FORMAT,
                            localeService.getLocaleForUser(user), "yyyy-MM-dd", dateString));
        }
        return theDate;
    }

    public static Set<String> getNamesOfThoseWithSignUps(Set<SignUp> signUpsAt, boolean includeEta) {
        final Set<String> signUpNames;
        signUpNames = new LinkedHashSet<>();
        for (SignUp signUp : signUpsAt) {
            if (signUp.getHowManyPeople() > 0) {
                String text = signUp.getUserName() + " (**" + signUp.getHowManyPeople();
                if (includeEta) {
                   text = text + ", ETA " + printTime(signUp.getArrivalTime());
                }
                text = text + "**)";
                signUpNames.add(text);
            }
        }
        return signUpNames;
    }

    public static String getPokemonIcon(Pokemon pokemon) {
        if (!pokemon.isEgg()) {
            return "https://pokemongohub.net/sprites/normal/" + pokemon.getNumber() + ".png";
        } else {
            return "https://pokeraidbot2.herokuapp.com/img/" + pokemon.getName().toLowerCase() + ".png";
        }
    }

    public static String[] prepareArguments(CommandEvent commandEvent) {
        return commandEvent.getArgs().replaceAll("\\s{2,4}", " ").split(" ");
    }

    public static boolean isRaidEx(Raid raid, PokemonRaidStrategyService strategyService, PokemonRepository pokemonRepository) {
        return isRaidExPokemon(raid.getPokemon().getName(), strategyService, pokemonRepository);
    }

    public static String preProcessTimeString(String timeString) {
        if (timeString != null && timeString.matches("[0-9]{3,4}")) {
            return new StringBuilder(timeString).insert(timeString.length()-2, ":").toString();
        } else {
            return timeString;
        }
    }

    public static void assertGroupStartNotBeforeRaidStart(LocalDateTime raidStart, LocalDateTime groupStart,
                                                          User user, LocaleService localeService) {
        if (raidStart.isAfter(groupStart)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_GROUP_BEFORE_RAID,
                            localeService.getLocaleForUser(user), printTimeIfSameDay(groupStart),
                            printTimeIfSameDay(raidStart)));
        }
    }

    public static void assertTimeInRaidTimespan(User user, LocalDateTime dateTimeToCheck, Raid raid,
                                                LocaleService localeService) {
        final LocalDateTime startOfRaid = getStartOfRaid(raid.getEndOfRaid(), raid.isExRaid());
        final boolean timeIsSameOrBeforeEnd =
                raid.getEndOfRaid().isAfter(dateTimeToCheck) || raid.getEndOfRaid().equals(dateTimeToCheck);
        final boolean timeIsSameOrAfterStart =
                startOfRaid.isBefore(dateTimeToCheck) || startOfRaid.equals(dateTimeToCheck);
        if (!(timeIsSameOrBeforeEnd && timeIsSameOrAfterStart)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.TIME_NOT_IN_RAID_TIMESPAN,
                    localeService.getLocaleForUser(user), printDateTime(dateTimeToCheck),
                    printDateTime(startOfRaid), printTimeIfSameDay(raid.getEndOfRaid())));
        }
    }

    public static Set<String> getResistantTo(PokemonTypes pokemonTypes) {
        return resistanceTable.getResistantTo(pokemonTypes);
    }

    public static boolean isExceptionOrCauseNetworkIssues(Throwable t) {
        return t != null && (t.getMessage().contains("SocketTimeoutException") || (isInstanceOfSocketException(t) ||
                (t.getCause() != null && isInstanceOfSocketException(t.getCause()))));
    }

    private static boolean isInstanceOfSocketException(Throwable t) {
        return (t instanceof SocketException) || (t instanceof SocketTimeoutException);
    }
} 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 UserMessedUpException extends RuntimeException {
    public UserMessedUpException(User user, String message) {
        super((user == null ? "" : (user.getName() + ": ")) + message);
    }

    public UserMessedUpException(String userName, String message) {
        super(userName + ": " + message);
    }
} public class Gym {
    private String name;
    private String id;
    private String x;
    private String y;
    private String area;
    private boolean exGym = false;

    public Gym(String name, String id, String x, String y, String area) {
        this.name = name;
        this.id = id;
        this.x = x;
        this.y = y;
        this.area = area;
    }

    public Gym(String name, String id, String x, String y, String area, boolean exGym) {
        this(name, id, x, y, area);
        this.exGym = exGym;
    }

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

        Gym gym = (Gym) o;

        if (exGym != gym.exGym) return false;
        if (name != null ? !name.equals(gym.name) : gym.name != null) return false;
        if (id != null ? !id.equals(gym.id) : gym.id != null) return false;
        if (x != null ? !x.equals(gym.x) : gym.x != null) return false;
        if (y != null ? !y.equals(gym.y) : gym.y != null) return false;
        return area != null ? area.equals(gym.area) : gym.area == null;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (id != null ? id.hashCode() : 0);
        result = 31 * result + (x != null ? x.hashCode() : 0);
        result = 31 * result + (y != null ? y.hashCode() : 0);
        result = 31 * result + (area != null ? area.hashCode() : 0);
        result = 31 * result + (exGym ? 1 : 0);
        return result;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getX() {
        return x;
    }

    public String getY() {
        return y;
    }

    public String getArea() {
        return area;
    }

    public boolean isInArea(String area) {
        if (!StringUtils.isEmpty(this.area)) {
            return this.area.equalsIgnoreCase(area);
        } else {
            return false;
        }
    }

    public boolean isExGym() {
        return exGym;
    }

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

    public String toStringDetails() {
        return "Gym{" +
                "name='" + name + '\'' +
                ", area='" + area + '\'' +
                ", id='" + id + '\'' +
                ", x='" + x + '\'' +
                ", y='" + y + '\'' +
                ", exGym=" + exGym +
                '}';
    }
} 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 Pokemon {

    private String name;
    private PokemonTypes types;
    private Set<String> weaknesses;
    private Set<String> resistant;
    private Integer number;
    private String about;
    private String buddyDistance;

    public Pokemon(Integer number, String name, String about, PokemonTypes types, String buddyDistance,
                   Set<String> weaknesses, Set<String> resistantTo) {
        Validate.notNull(number, "Number is null!");
        Validate.notNull(types, "Types is null!");
        Validate.notEmpty(name, "Name is empty!");
        if (types != PokemonTypes.NONE) {
            Validate.notEmpty(weaknesses, "Weaknesses is empty!");
            Validate.notEmpty(resistantTo, "ResistantTo is empty!");
        }

        this.number = number;
        this.name = name;
        this.about = about;
        this.types = types;
        this.buddyDistance = buddyDistance;
        this.weaknesses = weaknesses;
        resistant = resistantTo;
    }

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

        Pokemon pokemon = (Pokemon) o;

        if (name != null ? !name.equals(pokemon.name) : pokemon.name != null) return false;
        if (types != null ? !types.equals(pokemon.types) : pokemon.types != null) return false;
        if (weaknesses != null ? !weaknesses.equals(pokemon.weaknesses) : pokemon.weaknesses != null) return false;
        if (resistant != null ? !resistant.equals(pokemon.resistant) : pokemon.resistant != null) return false;
        if (number != null ? !number.equals(pokemon.number) : pokemon.number != null) return false;
        if (about != null ? !about.equals(pokemon.about) : pokemon.about != null) return false;
        return buddyDistance != null ? buddyDistance.equals(pokemon.buddyDistance) : pokemon.buddyDistance == null;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (types != null ? types.hashCode() : 0);
        result = 31 * result + (weaknesses != null ? weaknesses.hashCode() : 0);
        result = 31 * result + (resistant != null ? resistant.hashCode() : 0);
        result = 31 * result + (number != null ? number.hashCode() : 0);
        result = 31 * result + (about != null ? about.hashCode() : 0);
        result = 31 * result + (buddyDistance != null ? buddyDistance.hashCode() : 0);
        return result;
    }

    public PokemonTypes getTypes() {
        return types;
    }

    public Set<String> getWeaknesses() {
        return weaknesses;
    }

    public Set<String> getResistant() {
        return resistant;
    }

    public Integer getNumber() {
        return number;
    }

    public String getAbout() {
        return about;
    }

    public String getBuddyDistance() {
        return buddyDistance;
    }

    @Override
    public String toString() {
        return name + (types.getTypeSet().size() > 0 ? " (" + types + ")" : "");
    }

    public String getName() {
        return name;
    }

    public boolean isEgg() {
        return StringUtils.containsIgnoreCase(name, "egg") && name.matches("[Ee][Gg][Gg][1-6]");
    }
} 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 Raid {
    private final Pokemon pokemon;
    private final LocalDateTime endOfRaid;
    private final Gym gym;
    private final LocaleService localeService;
    private Map<String, SignUp> signUps = new ConcurrentHashMap<>();
    private String region;
    private String creator;
    private String id;
    private Boolean ex;

    public Raid(Pokemon pokemon, LocalDateTime endOfRaid, Gym gym, LocaleService localeService, String region, boolean ex) {
        this.pokemon = pokemon;
        this.endOfRaid = endOfRaid;
        this.gym = gym;
        this.localeService = localeService;
        this.region = region;
        this.ex = ex;
    }

    void setId(String id) {
        this.id = id;
    }

    public Pokemon getPokemon() {
        return pokemon;
    }

    public LocalDateTime getEndOfRaid() {
        return endOfRaid;
    }

    public Gym getGym() {
        return gym;
    }

    public String getCreator() {
        return creator;
    }

    void setCreator(String creator) {
        this.creator = creator;
    }

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

        if (o == null || getClass() != o.getClass()) return false;

        Raid raid = (Raid) o;

        return new EqualsBuilder()
                .append(pokemon, raid.pokemon)
                .append(endOfRaid, raid.endOfRaid)
                .append(gym, raid.gym)
                .append(localeService, raid.localeService)
                .append(signUps, raid.signUps)
                .append(region, raid.region)
                .append(creator, raid.creator)
                .append(id, raid.id)
                .append(ex, raid.ex)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37)
                .append(pokemon)
                .append(endOfRaid)
                .append(gym)
                .append(localeService)
                .append(signUps)
                .append(region)
                .append(creator)
                .append(id)
                .append(ex)
                .toHashCode();
    }

    @Override
    public String toString() {
        return localeService.getMessageFor(LocaleService.RAID_TOSTRING, LocaleService.DEFAULT, pokemon.toString(),
                gym.toString(), printDateTime(Utils.getStartOfRaid(endOfRaid, isExRaid())),
                printTime(endOfRaid.toLocalTime()));
    }

    public String toString(Locale locale) {
        return localeService.getMessageFor(LocaleService.RAID_TOSTRING, locale, pokemon.toString(),
                gym.toString(), Utils.printTimeIfSameDay(Utils.getStartOfRaid(endOfRaid, isExRaid())),
                Utils.printTimeIfSameDay(endOfRaid));
    }

    public void signUp(User user, int howManyPeople, LocalTime arrivalTime, RaidRepository repository) {
        SignUp signUp = signUps.get(user.getName());
        if (signUp != null) {
            int numberOfPeopleInSignup = signUp.getHowManyPeople();
            if (arrivalTime.equals(signUp.getArrivalTime())) {
                numberOfPeopleInSignup += howManyPeople;
            } else {
                numberOfPeopleInSignup = howManyPeople;
                signUp.setEta(arrivalTime);
            }
            Utils.assertNotTooManyOrNoNumber(user, localeService, String.valueOf(numberOfPeopleInSignup));
            signUp.setHowManyPeople(numberOfPeopleInSignup);
        } else {
            signUp = new SignUp(user.getName(), howManyPeople, arrivalTime);
            signUps.put(user.getName(), signUp);
        }
        repository.addSignUp(user, this, signUp);
    }

    public Set<SignUp> getSignUps() {
        return Collections.unmodifiableSet(new LinkedHashSet<>(signUps.values()));
    }

    public int getNumberOfPeopleSignedUp() {
        return signUps.values().stream().mapToInt(signup -> signup.getHowManyPeople()).sum();
    }

    public SignUp remove(User user, RaidRepository raidRepository) {
        final SignUp removed = signUps.remove(user.getName());
        if (removed != null) {
            raidRepository.removeSignUp(user, this, removed);
        }
        return removed;
    }

    public void setSignUps(Map<String, SignUp> signUps) {
        this.signUps = signUps;
    }

    public String getRegion() {
        return region;
    }

    public String getId() {
        return id;
    }

    public int getNumberOfPeopleArrivingAt(LocalTime eta) {
        return signUps.values().stream().mapToInt(signup ->
                signup.getArrivalTime().equals(eta) ? signup.getHowManyPeople() : 0)
                .sum();
    }

    public Set<SignUp> getSignUpsAt(LocalTime localTime) {
        return signUps.values().stream().filter(s -> s.getArrivalTime().equals(localTime)).collect(Collectors.toSet());
    }

    public boolean isExRaid() {
        return ex;
    }
} @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;
        }
    }
} @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);
    }
} public class Utils {
    public static final DateTimeFormatter timeParseFormatter = DateTimeFormatter.ofPattern("H[:][.]mm");
    public static final DateTimeFormatter dateAndTimeParseFormatter =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH[:][.]mm");
    public static final DateTimeFormatter timePrintFormatter = DateTimeFormatter.ofPattern("HH:mm");
    public static final DateTimeFormatter dateAndTimePrintFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
    public static final int HIGH_LIMIT_FOR_SIGNUPS = 20;
    public static final int RAID_DURATION_IN_MINUTES = 45;
    public static final int EX_RAID_DURATION_IN_MINUTES = 45;
    private static final String EX_RAID_BOSS = "deoxys";
    private static ClockService clockService = new ClockService();
    private static ResistanceTable resistanceTable = new ResistanceTable();

    public static ClockService getClockService() {
        return clockService;
    }

    public static void setClockService(ClockService clockService) {
        Utils.clockService = clockService;
    }

    public static String printDateTime(LocalDateTime dateTime) {
        return dateTime.format(dateAndTimePrintFormatter);
    }

    public static String printTime(LocalTime time) {
        return time.format(timePrintFormatter);
    }

    public static String printTimeIfSameDay(LocalDateTime dateAndTime) {
        if (dateAndTime.toLocalDate().isEqual(LocalDate.now())) {
            return dateAndTime.toLocalTime().format(timePrintFormatter);
        } else {
            return printDateTime(dateAndTime);
        }
    }

    public static boolean isTypeDoubleStrongVsPokemonWithTwoTypes(String pokemonTypeOne,
                                                                  String pokemonTypeTwo, String typeToCheck) {
        Validate.notEmpty(pokemonTypeOne);
        Validate.notEmpty(pokemonTypeTwo);
        Validate.notEmpty(typeToCheck);
        if (typeIsStrongVsPokemon(pokemonTypeOne, typeToCheck) && typeIsStrongVsPokemon(pokemonTypeTwo, typeToCheck)) {
            return true;
        } else {
            return false;
        }
    }

    public static Set<String> getWeaknessesFor(PokemonTypes pokemonType) {
        return resistanceTable.getWeaknesses(pokemonType);
    }

    public static boolean typeIsStrongVsPokemon(String pokemonType, String typeToCheck) {
        return resistanceTable.typeIsStrongVsPokemon(pokemonType, typeToCheck);
    }

    public static void assertSignupTimeNotBeforeRaidStartAndNow(User user, LocalDateTime dateAndTime,
                                                                LocalDateTime endOfRaid, LocaleService localeService,
                                                                boolean isExRaid) {
        final LocalDateTime startOfRaid = getStartOfRaid(endOfRaid, isExRaid);
        final LocalDateTime now = clockService.getCurrentDateTime();
        assertSignupTimeNotBeforeRaidStart(user, dateAndTime, endOfRaid, localeService, isExRaid);
        if (dateAndTime.isBefore(now)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.SIGN_BEFORE_NOW, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(now)));
        }
    }

    public static LocalDateTime getStartOfRaid(LocalDateTime endOfRaid, boolean isExRaid) {
        return isExRaid ? endOfRaid.minusMinutes(EX_RAID_DURATION_IN_MINUTES) :
                endOfRaid.minusMinutes(RAID_DURATION_IN_MINUTES);
    }

    public static void assertSignupTimeNotBeforeRaidStart(User user, LocalDateTime dateAndTime,
                                                          LocalDateTime endOfRaid, LocaleService localeService,
                                                          boolean isExRaid) {
        final LocalDateTime startOfRaid = getStartOfRaid(endOfRaid, isExRaid);
        if (dateAndTime.isBefore(startOfRaid)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.SIGN_BEFORE_RAID, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(startOfRaid)));
        }
    }

    public static void assertGroupTimeNotBeforeNow(User user, LocalDateTime dateAndTime,
                                                        LocaleService localeService) {
        final LocalDateTime now = clockService.getCurrentDateTime();
        if (dateAndTime.isBefore(now)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_GROUP_BEFORE_NOW, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(now)));
        }
    }

    public static void assertCreateRaidTimeNotBeforeNow(User user, LocalDateTime dateAndTime,
                                                        LocaleService localeService) {
        final LocalDateTime now = clockService.getCurrentDateTime();
        if (dateAndTime.isBefore(now)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.TIMEZONE, localeService.getLocaleForUser(user),
                            printTimeIfSameDay(dateAndTime), printTimeIfSameDay(now)));
        }
    }

    public static void assertTimeNotInNoRaidTimespan(User user, LocalTime time, LocaleService localeService) {
        if (time.isAfter(LocalTime.of(23, 0)) || time.isBefore(LocalTime.of(5, 0))) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAIDS_NOW, localeService.getLocaleForUser(user),
                            printTime(time)));
        }
    }

    public static void assertTimeNotMoreThanXHoursFromNow(User user, LocalTime time,
                                                          LocaleService localeService, Integer hours) {
        final LocalTime now = clockService.getCurrentTime();
        if (now.isBefore(LocalTime.of(22, 0)) && now.plusHours(2).isBefore(time)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_RAID_TOO_LONG, localeService.getLocaleForUser(user),
                            printTime(time), printTime(now), String.valueOf(hours)));
        }
    }

    public static void assertEtaNotAfterRaidEnd(User user, Raid raid, LocalDateTime eta, LocaleService localeService) {
        if (eta.isAfter(raid.getEndOfRaid())) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_ETA_AFTER_RAID,
                            localeService.getLocaleForUser(user), printTimeIfSameDay(eta),
                            printTimeIfSameDay(raid.getEndOfRaid())));
        }
    }

    public static String getStaticMapUrl(Gym gym) {
        // todo: host marker png via pokeraidbot web
        String url = "https://maps.googleapis.com/maps/api/staticmap?center=" + gym.getX() + "," + gym.getY() +
                "&zoom=14&size=400x400&maptype=roadmap&markers=icon:http://millert.se/pogo/marker_xsmall.png%7C" +
                gym.getX() + "," + gym.getY() + "&key=AIzaSyAZm7JLojr2KaUvkeHEpHh0Y-zPwP3dpCU";
        return url;
    }

    public static String getNonStaticMapUrl(Gym gym) {
        String url = "http://www.google.com/maps?q=" + gym.getX() + "," + gym.getY();
        return url;
    }

    public static String printWeaknesses(Pokemon pokemon) {
        Set<String> weaknessesToPrint = new LinkedHashSet<>();
        final Set<String> typeSet = pokemon.getTypes().getTypeSet();
        for (String weakness : pokemon.getWeaknesses()) {
            if (typeSet.size() > 1) {
                final Iterator<String> iterator = typeSet.iterator();
                if (isTypeDoubleStrongVsPokemonWithTwoTypes(iterator.next(), iterator.next(), weakness)) {
                    weaknessesToPrint.add("**" + weakness + "**");
                } else {
                    weaknessesToPrint.add(weakness);
                }
            } else {
                weaknessesToPrint.add(weakness);
            }
        }

        return StringUtils.join(weaknessesToPrint, ", ");
    }

    public static boolean isSamePokemon(String pokemonName, String existingEntityPokemon) {
        return pokemonName.equalsIgnoreCase(existingEntityPokemon);
    }

    public static boolean raidsCollide(LocalDateTime endOfRaid, boolean isExRaid, LocalDateTime endOfRaidTwo,
                                       boolean isExRaidTwo) {
        LocalDateTime startTime = getStartOfRaid(endOfRaid, isExRaid);
        LocalDateTime startTimeTwo = getStartOfRaid(endOfRaidTwo, isExRaidTwo);
        return isInInterval(startTime, endOfRaid, startTimeTwo, endOfRaidTwo) ||
                isInInterval(startTimeTwo, endOfRaidTwo, startTime, endOfRaid);
    }

    private static boolean isInInterval(LocalDateTime startTime, LocalDateTime endOfRaid,
                                        LocalDateTime startTimeTwo, LocalDateTime endOfRaidTwo) {
        return (startTime.isAfter(startTimeTwo) && startTime.isBefore(endOfRaidTwo)) ||
                (endOfRaid.isBefore(endOfRaidTwo) && endOfRaid.isAfter(startTimeTwo));
    }

    public static boolean isRaidExPokemon(String pokemonName, PokemonRaidStrategyService strategyService, PokemonRepository pokemonRepository) {
        Pokemon pokemon = pokemonRepository.getByName(pokemonName);
        return strategyService.getRaidInfo(pokemon).getBossTier() == 5;
    }

    public static LocalTime parseTime(User user, String timeString, LocaleService localeService) {
        LocalTime endsAtTime;
        try {
            timeString = preProcessTimeString(timeString);
            endsAtTime = LocalTime.parse(timeString, Utils.timeParseFormatter);
        } catch (DateTimeParseException | NullPointerException e) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.BAD_DATETIME_FORMAT, localeService.getLocaleForUser(user),
                            "HH:MM", timeString));
        }
        return endsAtTime;
    }

    public static Integer assertNotTooManyOrNoNumber(User user, LocaleService localeService, String people) {
        Integer numberOfPeople;
        try {
            numberOfPeople = new Integer(people);
            if (numberOfPeople < 1 || numberOfPeople > HIGH_LIMIT_FOR_SIGNUPS) {
                throw new RuntimeException();
            }
        } catch (RuntimeException e) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.ERROR_PARSE_PLAYERS,
                            localeService.getLocaleForUser(user),
                            people, String.valueOf(HIGH_LIMIT_FOR_SIGNUPS)));
        }
        return numberOfPeople;
    }

    public static LocalDate parseDate(User user, String dateString, LocaleService localeService) {
        LocalDate theDate;
        try {
            theDate = LocalDate.parse(dateString);
        } catch (DateTimeException | NullPointerException e) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.BAD_DATETIME_FORMAT,
                            localeService.getLocaleForUser(user), "yyyy-MM-dd", dateString));
        }
        return theDate;
    }

    public static Set<String> getNamesOfThoseWithSignUps(Set<SignUp> signUpsAt, boolean includeEta) {
        final Set<String> signUpNames;
        signUpNames = new LinkedHashSet<>();
        for (SignUp signUp : signUpsAt) {
            if (signUp.getHowManyPeople() > 0) {
                String text = signUp.getUserName() + " (**" + signUp.getHowManyPeople();
                if (includeEta) {
                   text = text + ", ETA " + printTime(signUp.getArrivalTime());
                }
                text = text + "**)";
                signUpNames.add(text);
            }
        }
        return signUpNames;
    }

    public static String getPokemonIcon(Pokemon pokemon) {
        if (!pokemon.isEgg()) {
            return "https://pokemongohub.net/sprites/normal/" + pokemon.getNumber() + ".png";
        } else {
            return "https://pokeraidbot2.herokuapp.com/img/" + pokemon.getName().toLowerCase() + ".png";
        }
    }

    public static String[] prepareArguments(CommandEvent commandEvent) {
        return commandEvent.getArgs().replaceAll("\\s{2,4}", " ").split(" ");
    }

    public static boolean isRaidEx(Raid raid, PokemonRaidStrategyService strategyService, PokemonRepository pokemonRepository) {
        return isRaidExPokemon(raid.getPokemon().getName(), strategyService, pokemonRepository);
    }

    public static String preProcessTimeString(String timeString) {
        if (timeString != null && timeString.matches("[0-9]{3,4}")) {
            return new StringBuilder(timeString).insert(timeString.length()-2, ":").toString();
        } else {
            return timeString;
        }
    }

    public static void assertGroupStartNotBeforeRaidStart(LocalDateTime raidStart, LocalDateTime groupStart,
                                                          User user, LocaleService localeService) {
        if (raidStart.isAfter(groupStart)) {
            throw new UserMessedUpException(user,
                    localeService.getMessageFor(LocaleService.NO_GROUP_BEFORE_RAID,
                            localeService.getLocaleForUser(user), printTimeIfSameDay(groupStart),
                            printTimeIfSameDay(raidStart)));
        }
    }

    public static void assertTimeInRaidTimespan(User user, LocalDateTime dateTimeToCheck, Raid raid,
                                                LocaleService localeService) {
        final LocalDateTime startOfRaid = getStartOfRaid(raid.getEndOfRaid(), raid.isExRaid());
        final boolean timeIsSameOrBeforeEnd =
                raid.getEndOfRaid().isAfter(dateTimeToCheck) || raid.getEndOfRaid().equals(dateTimeToCheck);
        final boolean timeIsSameOrAfterStart =
                startOfRaid.isBefore(dateTimeToCheck) || startOfRaid.equals(dateTimeToCheck);
        if (!(timeIsSameOrBeforeEnd && timeIsSameOrAfterStart)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.TIME_NOT_IN_RAID_TIMESPAN,
                    localeService.getLocaleForUser(user), printDateTime(dateTimeToCheck),
                    printDateTime(startOfRaid), printTimeIfSameDay(raid.getEndOfRaid())));
        }
    }

    public static Set<String> getResistantTo(PokemonTypes pokemonTypes) {
        return resistanceTable.getResistantTo(pokemonTypes);
    }

    public static boolean isExceptionOrCauseNetworkIssues(Throwable t) {
        return t != null && (t.getMessage().contains("SocketTimeoutException") || (isInstanceOfSocketException(t) ||
                (t.getCause() != null && isInstanceOfSocketException(t.getCause()))));
    }

    private static boolean isInstanceOfSocketException(Throwable t) {
        return (t instanceof SocketException) || (t instanceof SocketTimeoutException);
    }
}
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.command.CommandListener;
import main.BotServerMain;
import net.dv8tion.jda.api.entities.User;
import pokeraidbot.Utils;
import pokeraidbot.domain.config.LocaleService;
import pokeraidbot.domain.errors.UserMessedUpException;
import pokeraidbot.domain.gym.Gym;
import pokeraidbot.domain.gym.GymRepository;
import pokeraidbot.domain.pokemon.Pokemon;
import pokeraidbot.domain.pokemon.PokemonRepository;
import pokeraidbot.domain.raid.Raid;
import pokeraidbot.domain.raid.RaidRepository;
import pokeraidbot.infrastructure.jpa.config.Config;
import pokeraidbot.infrastructure.jpa.config.ServerConfigRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Locale;
import static pokeraidbot.Utils.*;
package pokeraidbot.commands;




/**
 * !raid new [Pokemon] [Ends in (HH:MM)] [Pokestop name]
 */
public class NewRaidCommand extends ConfigAwareCommand {
    private final GymRepository gymRepository;
    private final RaidRepository raidRepository;
    private final PokemonRepository pokemonRepository;
    private final LocaleService localeService;

    public NewRaidCommand(GymRepository gymRepository, RaidRepository raidRepository,
                          PokemonRepository pokemonRepository, LocaleService localeService,
                          ServerConfigRepository serverConfigRepository,
                          CommandListener commandListener) {
        super(serverConfigRepository, commandListener, localeService);
        this.pokemonRepository = pokemonRepository;
        this.localeService = localeService;
        this.name = "new";
        this.aliases = new String[]{"n", "e", "end", "endsat", "ends"};
        this.help = localeService.getMessageFor(LocaleService.NEW_RAID_HELP, LocaleService.DEFAULT);
        this.gymRepository = gymRepository;
        this.raidRepository = raidRepository;
    }

    @Override
    protected void executeWithConfig(CommandEvent commandEvent, Config config) {
        final User user = commandEvent.getAuthor();
        final String[] args = commandEvent.getArgs().replaceAll("\\s{1,3}", " ").split(" ");
        if (args.length < 3) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.BAD_SYNTAX,
                    localeService.getLocaleForUser(user), "!raid new ho-oh 10:00 solna platform"));
        }
        String pokemonName = args[0];
        final Pokemon pokemon = pokemonRepository.search(pokemonName, user);
        String timeString = args[1];
        LocalTime endsAtTime = Utils.parseTime(user, timeString, localeService);
        LocalDateTime endsAt = LocalDateTime.of(LocalDate.now(), endsAtTime);

        assertTimeNotInNoRaidTimespan(user, endsAtTime, localeService);
        assertTimeNotMoreThanXHoursFromNow(user, endsAtTime, localeService, 2);
        assertCreateRaidTimeNotBeforeNow(user, endsAt, localeService);

        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 = new Raid(pokemon, endsAt, gym, localeService, config.getRegion(), false);