@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 BotService {
    private static final Logger LOGGER = LoggerFactory.getLogger(BotService.class);
    private final Set<EventListener> extraListeners = new CopyOnWriteArraySet<>();
    private TrackingService trackingService;
    private String ownerId;
    private String token;
    private JDA botInstance;
    private CommandClient commandClient;
    private CommandListener aggregateCommandListener;
    private GymRepository gymRepository;
    private ServerConfigRepository serverConfigRepository;
    private UserConfigRepository userConfigRepository;
    public static List<String> currentTier5Bosses = new CopyOnWriteArrayList<>();
    static {
        if (LocalDate.now().isAfter(LocalDate.of(2018, Month.DECEMBER, 18)) &&
                LocalDate.now().isBefore(LocalDate.of(2018, Month.JANUARY, 19))) {
            currentTier5Bosses.add("Heatran");
        }
    }

    public BotService(LocaleService localeService, GymRepository gymRepository, RaidRepository raidRepository,
                      PokemonRepository pokemonRepository, PokemonRaidStrategyService raidInfoService,
                      ServerConfigRepository serverConfigRepository, UserConfigRepository userConfigRepository,
                      ExecutorService executorService, ClockService clockService, TrackingService trackingService,
                      String ownerId, String token) {
        this.gymRepository = gymRepository;
        this.serverConfigRepository = serverConfigRepository;
        this.userConfigRepository = userConfigRepository;
        this.trackingService = trackingService;
        this.ownerId = ownerId;
        this.token = token;
        if (!System.getProperty("file.encoding").equals("UTF-8")) {
            System.err.println("ERROR: Not using UTF-8 encoding");
            System.exit(-1);
        }

        initializeConfig();

        EventWaiter waiter = new EventWaiter();
        // For detailed logging - used during debugging/development
//        EventLoggingListener eventLoggingListener = new EventLoggingListener();
        GymHuntrRaidEventListener gymHuntrRaidEventListener = new GymHuntrRaidEventListener(
                serverConfigRepository, raidRepository, gymRepository, pokemonRepository, localeService,
                executorService,
                clockService, this, raidInfoService);
        StartUpEventListener startUpEventListener = new StartUpEventListener(serverConfigRepository,
                raidRepository, localeService, clockService, executorService, this, gymRepository,
                pokemonRepository, raidInfoService);
        SignupWithPlusCommandListener plusCommandEventListener = new SignupWithPlusCommandListener(raidRepository,
                pokemonRepository, serverConfigRepository, this, localeService);
        UnsignWithMinusCommandListener minusCommandEventListener = new UnsignWithMinusCommandListener(raidRepository,
                pokemonRepository, serverConfigRepository, this, localeService);
        aggregateCommandListener = new AggregateCommandListener(Arrays.asList());

        CommandClientBuilder client = new CommandClientBuilder();
        client.setOwnerId(this.ownerId);
        client.setEmojis(Emotes.OK, "\uD83D\uDE2E", Emotes.ERROR);
        client.setPrefix("!raid ");
        client.setAlternativePrefix("!r ");
        client.setActivity(Activity.playing("Type !raid usage"));
        client.addCommands(
                new WhatsNewCommand(serverConfigRepository, aggregateCommandListener, localeService),
                new HelpManualCommand(localeService, serverConfigRepository, aggregateCommandListener),
                new AboutCommand(
                        Color.BLUE, localeService.getMessageFor(LocaleService.AT_YOUR_SERVICE, LocaleService.DEFAULT),
                        new String[]{LocaleService.featuresString_SV}, Permission.ADMINISTRATOR
                ),
                new PingCommand(),
                new UsageCommand(localeService, serverConfigRepository, aggregateCommandListener),
                new GettingStartedCommand(localeService, serverConfigRepository, aggregateCommandListener),
                new AdminCommands(userConfigRepository, serverConfigRepository, gymRepository,
                        this, trackingService, localeService, pokemonRepository, raidRepository),
                new NewRaidCommand(gymRepository, raidRepository, pokemonRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new NewRaidStartsAtCommand(gymRepository, raidRepository, pokemonRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new NewRaidExCommand(gymRepository, raidRepository, pokemonRepository, localeService,
                        serverConfigRepository, aggregateCommandListener, raidInfoService),
                new UserConfigCommand(serverConfigRepository, aggregateCommandListener, localeService,
                        userConfigRepository),
                new RaidStatusCommand(gymRepository, raidRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new RaidListCommand(raidRepository, localeService, serverConfigRepository, pokemonRepository,
                        aggregateCommandListener),
                new PotentialExRaidListCommand(raidRepository, localeService, serverConfigRepository,
                        gymRepository, aggregateCommandListener),
                new ExRaidListCommand(localeService, serverConfigRepository,
                        gymRepository, aggregateCommandListener),
                new SignUpCommand(gymRepository, raidRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new WhereIsGymCommand(gymRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new WhereIsGymInChatCommand(gymRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new RemoveSignUpCommand(gymRepository, raidRepository, localeService,
                        serverConfigRepository, aggregateCommandListener),
                new PokemonVsCommand(pokemonRepository, raidInfoService, localeService, serverConfigRepository,
                        aggregateCommandListener),
                new ServerInfoCommand(serverConfigRepository, localeService, aggregateCommandListener, clockService),
                new DonateCommand(localeService, serverConfigRepository, aggregateCommandListener),
                new TrackPokemonCommand(serverConfigRepository, localeService, pokemonRepository,
                        trackingService, aggregateCommandListener),
                new UnTrackPokemonCommand(serverConfigRepository, localeService, pokemonRepository,
                        aggregateCommandListener, trackingService),
                new InstallCommand(serverConfigRepository, gymRepository),
                new InstallEmotesCommand(localeService),
                new AlterRaidCommand(gymRepository, raidRepository, pokemonRepository, localeService, serverConfigRepository,
                        aggregateCommandListener, this),
                new NewRaidGroupCommand(gymRepository, raidRepository, pokemonRepository, localeService,
                        serverConfigRepository, aggregateCommandListener, this, clockService,
                        executorService, raidInfoService),
                new StartRaidAndCreateGroupCommand(gymRepository, raidRepository, pokemonRepository, localeService,
                        serverConfigRepository, aggregateCommandListener, this, clockService,
                        executorService, raidInfoService),
                new EggHatchedCommand(gymRepository, raidRepository, pokemonRepository, localeService,
                        serverConfigRepository,
                        aggregateCommandListener, raidInfoService),
                new RaidOverviewCommand(raidRepository, localeService, serverConfigRepository,
                        aggregateCommandListener, clockService, executorService, raidInfoService),
                new GymCommands(localeService, serverConfigRepository, gymRepository, aggregateCommandListener)
        );

        try {
            commandClient = client.build();
            LOGGER.info("Building JDA client...");
            botInstance = JDABuilder.createDefault(this.token)
                    // set the game for when the bot is loading
                    .setStatus(OnlineStatus.DO_NOT_DISTURB)
                    .setActivity(Activity.playing("loading..."))

                    // Network-related settings
                    .setRequestTimeoutRetry(true)
                    .setAutoReconnect(true)

                    // add the listeners
                    .addEventListeners(waiter,
                            commandClient,
//                    .addEventListener(eventLoggingListener)
                    startUpEventListener,
                    plusCommandEventListener,
                    minusCommandEventListener,
                    gymHuntrRaidEventListener)

                    // start it up!
                    .build().awaitReady();
            LOGGER.info("JDA client started.");

            LOGGER.info("Setting up event listeners...");
            for (EventListener extraListener : extraListeners) {
                botInstance.addEventListener(extraListener);
                LOGGER.info("Added extra event listener after initialization: " + extraListener);
            }
        } catch (LoginException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Transactional
    public void initializeConfig() {
        if (serverConfigRepository.findAll().size() == 0) {
            LOGGER.warn("Could not find any configuration in database, assuming fresh install. " +
                    "Creating basic server configurations..");
            // My test servers
            serverConfigRepository.save(new Config("manhattan_new_york", false,
                    Locale.ENGLISH, "pokeraidbot_us_test"));
            serverConfigRepository.save(new Config("uppsala", "zhorhn tests stuff"));
            serverConfigRepository.save(new Config("uppsala", "pokeraidbot_lab2"));
            serverConfigRepository.save(new Config("uppsala", "pokeraidbot_stage"));
            serverConfigRepository.save(new Config("uppsala", "pokeraidbot_test"));

            LOGGER.info("Server configurations created. Add more via the command for an administrator " +
                    "in a server where pokeraidbot has been added: !raid install");
        }
        gymRepository.reloadGymData();
    }

    public JDA getBot() {
        if (botInstance == null) {
            throw new IllegalStateException("Bot instance has not yet been initialized!");
        }
        return botInstance;
    }

    public CommandClient getCommandClient() {
        return commandClient;
    }

    public void addExtraListenerToBeAddedAfterStartUp(EventListener listener) {
        extraListeners.add(listener);
    }

    public void addEmoticonEventListener(EmoticonSignUpMessageListener emoticonSignUpMessageListener) {
        if (botInstance == null) {
            addExtraListenerToBeAddedAfterStartUp(emoticonSignUpMessageListener);
        } else {
            botInstance.addEventListener(emoticonSignUpMessageListener);
        }
    }
} 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 NewRaidGroupCommand extends ConcurrencyAndConfigAwareCommand {
    private static final Logger LOGGER = LoggerFactory.getLogger(NewRaidGroupCommand.class);

    private final GymRepository gymRepository;
    private final RaidRepository raidRepository;
    private final PokemonRepository pokemonRepository;
    private final LocaleService localeService;
    private final BotService botService;
    private final ClockService clockService;
    private final PokemonRaidStrategyService pokemonRaidStrategyService;

    public NewRaidGroupCommand(GymRepository gymRepository, RaidRepository raidRepository,
                               PokemonRepository pokemonRepository, LocaleService localeService,
                               ServerConfigRepository serverConfigRepository,
                               CommandListener commandListener, BotService botService,
                               ClockService clockService, ExecutorService executorService,
                               PokemonRaidStrategyService pokemonRaidStrategyService) {
        super(serverConfigRepository, commandListener, localeService, executorService);
        this.pokemonRepository = pokemonRepository;
        this.localeService = localeService;
        this.botService = botService;
        this.clockService = clockService;
        this.pokemonRaidStrategyService = pokemonRaidStrategyService;
        this.name = "group";
        this.aliases = new String[]{"g"};
        this.help = localeService.getMessageFor(LocaleService.RAID_GROUP_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().split(" ");
        final Locale locale = localeService.getLocaleForUser(user);
        if (args.length < 2) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.BAD_SYNTAX,
                    localeService.getLocaleForUser(user), "!raid group 10:00 solna platform"));
        }

        String timeString = args[0];
        LocalTime startAtTime = Utils.parseTime(user, timeString, localeService);

        assertTimeNotInNoRaidTimespan(user, startAtTime, localeService);

        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());
        final Raid raid = raidRepository.getActiveRaidOrFallbackToExRaid(gym, config.getRegion(), user);
        createRaidGroup(commandEvent.getChannel(), commandEvent.getGuild(),
                config, user, locale, startAtTime, raid.getId(),
                localeService, raidRepository, botService, serverConfigRepository, pokemonRepository, gymRepository,
                clockService, executorService, pokemonRaidStrategyService);
        commandEvent.reactSuccess();
        removeOriginMessageIfConfigSaysSo(config, commandEvent);
    }

    public static void createRaidGroup(MessageChannel channel, Guild guild, Config config, User user,
                                       Locale locale, LocalTime startAtTime, String raidId, LocaleService localeService,
                                       RaidRepository raidRepository, BotService botService,
                                       ServerConfigRepository serverConfigRepository,
                                       PokemonRepository pokemonRepository, GymRepository gymRepository,
                                       ClockService clockService, ExecutorService executorService,
                                       PokemonRaidStrategyService pokemonRaidStrategyService) {
        assertAllParametersNotNullOrEmpty(channel, config, user, locale, startAtTime, raidId, localeService,
                raidRepository, botService, serverConfigRepository, pokemonRepository, gymRepository,
                clockService, executorService);

        Raid raid = raidRepository.getById(raidId);
        final LocalDate raidDate = raid.getEndOfRaid().toLocalDate();
        final LocalDateTime raidStart = Utils.getStartOfRaid(raid.getEndOfRaid(), raid.isExRaid());
        LocalDateTime startAt = LocalDateTime.of(raidDate, startAtTime);
        checkValidInput(user, locale, startAtTime, localeService, raidRepository, raid, raidStart, startAt);

        final EmoticonSignUpMessageListener emoticonSignUpMessageListener =
                new EmoticonSignUpMessageListener(botService, localeService,
                        serverConfigRepository, raidRepository, pokemonRepository, gymRepository,
                        raid.getId(), startAt, user);
        TimeUnit delayTimeUnit = raid.isExRaid() ? TimeUnit.MINUTES : TimeUnit.SECONDS;
        int delay = raid.isExRaid() ? 1 : 15;
        final MessageEmbed messageEmbed = getRaidGroupMessageEmbed(startAt, raid.getId(), localeService,
                clockService, locale, delayTimeUnit, delay, raidRepository);
        channel.sendMessage(messageEmbed).queue(embed -> {
            final String messageId = embed.getId();
            emoticonSignUpMessageListener.setInfoMessageId(messageId);
            emoticonSignUpMessageListener.setEmoteMessageId(messageId);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Thread: " + Thread.currentThread().getId() +
                        " - Adding event listener and emotes for emote message with ID: " + messageId);
            }
            final MessageChannel embedChannel = embed.getChannel();
            RaidGroup group = new RaidGroup(config.getServer(), embedChannel.getName(),
                    messageId, messageId, user.getId(), startAt);
            group = raidRepository.newGroupForRaid(user, group, raid, guild, config);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Created group in channel " + channel.getName() +
                        " for emote message with ID: " + messageId + " - " + group);
            }
            // Add number icons for signups
            embedChannel.addReactionById(messageId, Emotes.ONE).queue();
            embedChannel.addReactionById(messageId, Emotes.TWO).queue();
            embedChannel.addReactionById(messageId, Emotes.THREE).queue();
            embedChannel.addReactionById(messageId, Emotes.FOUR).queue();
            embedChannel.addReactionById(messageId, Emotes.FIVE).queue();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Eventlistener and emotes added for emote message with ID: " + messageId);
            }
            if (config.isPinGroups()) {
                embedChannel.pinMessageById(embed.getId()).queueAfter(50, TimeUnit.MILLISECONDS);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Pinning info message for raid group. ID is: " + embed.getId());
                }
            }
            final Callable<Boolean> refreshEditThreadTask =
                        getMessageRefreshingTaskToSchedule(channel,
                                raid, emoticonSignUpMessageListener, messageId,
                                locale,
                                raidRepository, localeService,
                                clockService, executorService, botService,
                                delayTimeUnit, delay, group.getId());
            executorService.submit(refreshEditThreadTask);
        });
    }

    private static void checkValidInput(User user, Locale locale, LocalTime startAtTime,
                                        LocaleService localeService, RaidRepository raidRepository,
                                        Raid raid, LocalDateTime raidStart, LocalDateTime startAt) {
        Utils.assertGroupStartNotBeforeRaidStart(raidStart, startAt, user, localeService);
        if (!raid.isExRaid()) {
            assertTimeNotMoreThanXHoursFromNow(user, startAtTime, localeService, 2);
        }
        assertGroupTimeNotBeforeNow(user, startAt, localeService);
        if (!startAt.isBefore(raid.getEndOfRaid())) {
            final String errorText = localeService.getMessageFor(LocaleService.CANT_CREATE_GROUP_LATE,
                    locale);
            throw new UserMessedUpException(user, errorText);
        }

        if (raidRepository.userHasGroupForRaid(user, raid)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.TOO_MANY_GROUPS,
                    localeService.getLocaleForUser(user)));
        }

        if (raidRepository.existsGroupForRaidAt(raid, startAt)) {
            throw new UserMessedUpException(user, localeService.getMessageFor(LocaleService.GROUP_NOT_ADDED,
                    localeService.getLocaleForUser(user), String.valueOf(raid)));
        }
    }

    private static void assertAllParametersNotNullOrEmpty(MessageChannel channel, Config config, User user, Locale locale,
                                                          LocalTime startAtTime, String raidId, LocaleService localeService,
                                                          RaidRepository raidRepository, BotService botService,
                                                          ServerConfigRepository serverConfigRepository,
                                                          PokemonRepository pokemonRepository, GymRepository gymRepository,
                                                          ClockService clockService, ExecutorService executorService) {
        Validate.notNull(channel, "Channel");
        Validate.notNull(config, "config");
        Validate.notNull(user, "User");
        Validate.notNull(locale, "Locale");
        Validate.notNull(startAtTime, "StartAtTime");
        Validate.notNull(localeService, "LocaleService");
        Validate.notNull(raidRepository, "RaidRepository");
        Validate.notNull(botService, "BotService");
        Validate.notNull(serverConfigRepository, "ServerConfigRepository");
        Validate.notNull(pokemonRepository, "PokemonRepository");
        Validate.notNull(gymRepository, "GymRepository");
        Validate.notNull(clockService, "ClockService");
        Validate.notNull(executorService, "ExecutorService");
        Validate.notEmpty(raidId, "Raid ID");
    }

    public static Callable<Boolean> getMessageRefreshingTaskToSchedule(MessageChannel messageChannel,
                                                                       Raid raid,
                                                                       EmoticonSignUpMessageListener emoticonSignUpMessageListener,
                                                                       String infoMessageId, Locale locale,
                                                                       RaidRepository raidRepository,
                                                                       LocaleService localeService,
                                                                       ClockService clockService,
                                                                       ExecutorService executorService,
                                                                       BotService botService,
                                                                       TimeUnit delayTimeUnit, int delay,
                                                                       String raidGroupId) {
        Callable<Boolean> refreshEditThreadTask = () -> {
            final String groupId = raidGroupId;
            final Raid currentStateOfRaid = raidRepository.getById(raid.getId());
            final Callable<Boolean> editTask = () -> {
                delayTimeUnit.sleep(delay);
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Thread: " + Thread.currentThread().getId() +
                            " - Updating for group at gym " + currentStateOfRaid.getGym().getName() +
                            ": message ID=" + infoMessageId);
                }
                LocalDateTime start = emoticonSignUpMessageListener.getStartAt();
                final MessageEmbed newContent =
                        getRaidGroupMessageEmbed(start, raid.getId(),
                                localeService, clockService, locale, delayTimeUnit, delay, raidRepository
                        );
                messageChannel.editMessageById(infoMessageId,
                        newContent)
                        .queue(m -> {
                            if (LOGGER.isTraceEnabled()) {
                                LOGGER.trace("Message edit ok for " + infoMessageId);
                            }
                        }, m -> {
                            LOGGER.warn(m.getClass().getName() + " occurred in edit message loop: " + m.getMessage());
                            if (Utils.isExceptionOrCauseNetworkIssues(m)) {
                                LOGGER.info("Exception was due to timeout, so trying again soon. Could be temporary.");
                                try {
                                    // If we get a timeout, it's probably due to discord having issues.
                                    // Try to delay 60 seconds and try again.
                                    LOGGER.debug("Since we had a timeout, trying to wait 60 seconds before trying again.");
                                    delayTimeUnit.sleep(60000);
                                } catch (InterruptedException e) {
                                }
                            } else {
                                LOGGER.info("Exception was not due to timeout, so terminating this group.");
                                emoticonSignUpMessageListener.setStartAt(null);
                                LOGGER.info("Raid group will now be cleaned up for raid: " +
                                        raidCleanUpInfo(raid, emoticonSignUpMessageListener));
                                cleanUpRaidGroupAndDeleteSignUpsIfPossible(messageChannel,
                                        emoticonSignUpMessageListener.getStartAt(),
                                        currentStateOfRaid != null ? currentStateOfRaid.getId() : null,
                                        emoticonSignUpMessageListener, raidRepository, botService, groupId);
                            }
                        });
                return true;
            };
            while (raidIsActiveAndRaidGroupNotExpired(currentStateOfRaid.getEndOfRaid(),
                    emoticonSignUpMessageListener.getStartAt(), clockService)) {
                try {
                    executorService.submit(editTask).get();
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace("Edit task done for now: " + infoMessageId);
                    }
                } catch (InterruptedException | ExecutionException e) {
                    throw new RuntimeException(e);
                }
            }
            LOGGER.info("Raid group will now be cleaned up for raid: " +
                    raidCleanUpInfo(raid, emoticonSignUpMessageListener));
            cleanUpRaidGroupAndDeleteSignUpsIfPossible(messageChannel, emoticonSignUpMessageListener.getStartAt(),
                    raid != null ? raid.getId() : null,
                    emoticonSignUpMessageListener, raidRepository, botService, groupId);
            return true;
        };
        return refreshEditThreadTask;
    }

    private static String raidCleanUpInfo(Raid raid, EmoticonSignUpMessageListener emoticonSignUpMessageListener) {
        return (raid == null ? emoticonSignUpMessageListener.getRaidId() : raid) +
                ", group at time " +
                (emoticonSignUpMessageListener.getStartAt() == null ? "N/A" :
                        printTime(emoticonSignUpMessageListener.getStartAt().toLocalTime())) +
                " and creator: " + emoticonSignUpMessageListener.getUserId();
    }

    private static boolean raidIsActiveAndRaidGroupNotExpired(LocalDateTime endOfRaid,
                                                              LocalDateTime raidGroupStartTime,
                                                              ClockService clockService) {
        final LocalDateTime currentDateTime = clockService.getCurrentDateTime();
        return raidGroupStartTime != null &&
                currentDateTime.isBefore(
                        raidGroupStartTime.plusMinutes(5))
                // 20 seconds here to match the 15 second sleep for the edit task
                && currentDateTime.isBefore(endOfRaid.minusSeconds(20));
    }

    public static void cleanUpRaidGroupAndDeleteSignUpsIfPossible(MessageChannel messageChannel,
                                                                  LocalDateTime startAt, String raidId,
                                                                  EmoticonSignUpMessageListener emoticonSignUpMessageListener,
                                                                  RaidRepository raidRepository,
                                                                  BotService botService, String groupId) {
        Raid raid = null;
        try {
            if (startAt != null && raidId != null) {
                // Clean up all signups that should have done their raid now, if there still is a time
                // (Could be set to null due to an error, in that case keep signups in database)
                raid = raidRepository.removeAllSignUpsAt(raidId, startAt);
            }
        } catch (Throwable t) {
            // Do nothing, just log
            LOGGER.warn("Exception occurred when removing signups: " + t + "-" + t.getMessage());
            if (t instanceof ConcurrentModificationException) {
                LOGGER.warn("This is probably due to raid being removed while cleaning up signups, which is normal.");
            }
        } finally {
            final String raidDescription = raid == null ? "null" : raid.toString();
            cleanUpGroupMessageAndEntity(messageChannel, raidId, emoticonSignUpMessageListener, raidRepository,
                    botService, groupId, raidDescription);
        }
    }

    public static void cleanUpGroupMessageAndEntity(MessageChannel messageChannel, String raidId,
                                                       EmoticonSignUpMessageListener emoticonSignUpMessageListener,
                                                       RaidRepository raidRepository, BotService botService,
                                                       String groupId, String raidDescription) {
        // Clean up after raid expires
        final String infoMessageId = emoticonSignUpMessageListener.getInfoMessageId();
        if (!StringUtils.isEmpty(infoMessageId)) {
            try {
                messageChannel.deleteMessageById(infoMessageId).queue(m -> {}, t -> {
                    LOGGER.warn("Exception occurred when removing group message: " + t.getMessage());
                    botService.getBot().removeEventListener(emoticonSignUpMessageListener);
                    raidRepository.deleteGroupInNewTransaction(raidId, groupId);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Cleaned up listener related to this group.");
                    }
                });
            } catch (Throwable t) {
                LOGGER.warn("Exception occurred when removing group message: " + t.getMessage());
            }
        } else {
            LOGGER.warn("Group message Id was null for raid group for raid: " +
                    emoticonSignUpMessageListener.getRaidId());
        }
        botService.getBot().removeEventListener(emoticonSignUpMessageListener);
        raidRepository.deleteGroupInNewTransaction(raidId, groupId);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Cleaned up listener and message related to the group with ID " + groupId +
                    " for raid " + raidDescription);
        }
    }

    private static MessageEmbed getRaidGroupMessageEmbed(LocalDateTime startAt, String raidId,
                                                         LocaleService localeService, ClockService clockService,
                                                         Locale locale, TimeUnit delayTimeUnit,
                                                         int delay, RaidRepository raidRepository) {
        Raid currentStateOfRaid = raidRepository.getById(raidId);
        final Gym gym = currentStateOfRaid.getGym();
        final Pokemon pokemon = currentStateOfRaid.getPokemon();
        MessageEmbed messageEmbed;
        EmbedBuilder embedBuilder = new EmbedBuilder();
        final String headline = localeService.getMessageFor(LocaleService.GROUP_HEADLINE,
                locale, currentStateOfRaid.getPokemon().getName(), gym.getName()
                        + (gym.isExGym() ? Emotes.STAR : ""));
        final String getHereText = localeService.getMessageFor(LocaleService.GETTING_HERE,
                locale);
        embedBuilder.setTitle(getHereText + " " + gym.getName(),
                Utils.getNonStaticMapUrl(gym));
        final LocalDateTime endOfRaid = currentStateOfRaid.getEndOfRaid();
        embedBuilder.setAuthor(headline + " " +
                Utils.printTime(Utils.getStartOfRaid(endOfRaid, currentStateOfRaid.isExRaid()).toLocalTime()) +
                "-" + Utils.printTime(endOfRaid.toLocalTime()), null, Utils.getPokemonIcon(pokemon));
        final Set<SignUp> signUpsAt = currentStateOfRaid.getSignUpsAt(startAt.toLocalTime());
        final Set<String> signUpNames = getNamesOfThoseWithSignUps(signUpsAt, false);
        final String allSignUpNames = signUpNames.size() > 0 ? StringUtils.join(signUpNames, ", ") : "-";
        final int numberOfPeopleArrivingAt = signUpsAt.stream().mapToInt(SignUp::getHowManyPeople).sum();
        final String numberOfSignupsText = localeService.getMessageFor(LocaleService.SIGNED_UP,
                locale);
        final String totalSignUpsText = numberOfSignupsText + ": " + numberOfPeopleArrivingAt;
        final String thoseWhoAreComingText = localeService.getMessageFor(LocaleService.WHO_ARE_COMING,
                locale) + ":";
        embedBuilder.clearFields();
        final String handleSignUpText =
                localeService.getMessageFor(LocaleService.HANDLE_SIGNUP, locale);
        final Set<RaidGroup> groups = raidRepository.getGroups(currentStateOfRaid);
        final String descriptionAppendixText;
        if (groups.size() > 1) {
            descriptionAppendixText = raidRepository.listGroupsForRaid(currentStateOfRaid, groups);
        } else {
            descriptionAppendixText = "- " + handleSignUpText;
        }
        embedBuilder.setDescription("**Start: " + printTimeIfSameDay(startAt) + "** " + descriptionAppendixText);
        embedBuilder.addField(totalSignUpsText + ". " + thoseWhoAreComingText, allSignUpNames, true);
        final String updatedMessage = localeService.getMessageFor(LocaleService.UPDATED_EVERY_X,
                locale, LocaleService.asString(delayTimeUnit, locale),
                String.valueOf(delay)) + " " + localeService.getMessageFor(LocaleService.LAST_UPDATE,
                locale,
                printTime(clockService.getCurrentTime())) + ".";

        embedBuilder.setFooter(updatedMessage, null);
        messageEmbed = embedBuilder.build();
        return messageEmbed;
    }
} public class ClockService {
    private LocalTime mockTime = null;

    public ClockService() {
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        public String getMessageKey() {
            return messageKey;
        }

        public Locale getLocale() {
            return locale;
        }

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

            I18nLookup that = (I18nLookup) o;

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

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

    public static String featuresString_EN =
            "Pokeraidbot - a Discord bot to help Pokémon Go raiders. Raid, map, pokemon info functions.\n\n" +
                    "Getting started guide: " +
                    "https://github.com/magnusmickelsson/pokeraidbot/blob/master/GETTING_STARTED_USER_en.md\n\n" +
                    "**Get detailed help about how the bot works:**\n!raid man\n\n" +
                    "**To see one of its features, type the following:** " +
                    "!raid map {name of a raid gym in your vicinity}\n" +
                    "*Example:* !raid map Solna Platform\n\n" +
                    "https://github.com/magnusmickelsson/pokeraidbot to report errors, request features, " +
                    "see screenshots of usage etc.\n\n" +
            "**How do I support development of this bot?**\n!raid donate";
    public static String featuresString_SV =
            "Pokeraidbot - en Discord-bot för att hjälpa Pokémon Go raiders, med t.ex. kartor till gym, raidplanering" +
                    " och information om pokemons.\n\n" +
                    "Kom igång guide: " +
                    "https://github.com/magnusmickelsson/pokeraidbot/blob/master/GETTING_STARTED_USER_sv.md\n\n" +
                    "**Få detaljerad hjälp om hur botten fungerar:**\n!raid man {frivilligt: ämne}\n" +
                    "*Exempel:* !raid man - berättar om hur man använder raid man och vilka hjälpämnen som finns\n\n" +
                    "**För ett exempel på vad botten kan göra, skriv:** !raid map {namn på ett gym i området}\n" +
                    "*Exempel:* !raid map Solna Platform\n\n" +
                    "https://github.com/magnusmickelsson/pokeraidbot för att rapportera fel, önska funktioner, " +
                    "se screenshots av användning etc.\n\n" +
            "**Hur kan jag stödja utveckling av botten?**\n!raid donate\n\n" +
            "**If you want this information in english:**\n!raid usage en";
} public class 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 PokemonRaidInfo {
    private Pokemon pokemon;
    private int maxCp;
    private int bossTier;

    public PokemonRaidInfo(Pokemon pokemon, String maxCp, int bossTier) {
        this(pokemon, Integer.parseInt(maxCp), bossTier);
    }

    public PokemonRaidInfo(Pokemon pokemon, int maxCp, int bossTier) {
        Validate.notNull(pokemon, "Pokemon");
        Validate.isTrue(maxCp > 0 && maxCp < 4000, "Max CP for raidboss is in interval 0 to 3000");
        Validate.isTrue(bossTier > 0 && bossTier <= 6, "Raid bosses are in tier 1 to 6"); // Support mega bosses
        this.pokemon = pokemon;
        this.maxCp = maxCp;
        this.bossTier = bossTier;
    }

    public Pokemon getPokemon() {
        return pokemon;
    }

    public int getMaxCp() {
        return maxCp;
    }

    public int getBossTier() {
        return bossTier;
    }

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

        PokemonRaidInfo that = (PokemonRaidInfo) o;

        if (maxCp != that.maxCp) return false;
        if (bossTier != that.bossTier) return false;
        return pokemon != null ? pokemon.equals(that.pokemon) : that.pokemon == null;
    }

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

    @Override
    public String toString() {
        return "PokemonRaidInfo{" +
                "pokemon=" + pokemon +
                ", maxCp=" + maxCp +
                ", bossTier=" + bossTier +
                '}';
    }
} public class PokemonRepository {
    private static final Logger LOGGER = LoggerFactory.getLogger(PokemonRepository.class);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    public PokemonRaidInfo getRaidInfo(Pokemon pokemon) {
        final String pokemonName = pokemon.getName().toUpperCase();
        return pokemonRaidInfo.get(pokemonName);
    }
} 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 main.BotServerMain;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.SelfUser;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.Event;
import net.dv8tion.jda.api.events.GenericEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.EventListener;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pokeraidbot.BotService;
import pokeraidbot.Utils;
import pokeraidbot.commands.NewRaidGroupCommand;
import pokeraidbot.domain.config.ClockService;
import pokeraidbot.domain.config.LocaleService;
import pokeraidbot.domain.gym.Gym;
import pokeraidbot.domain.gym.GymRepository;
import pokeraidbot.domain.pokemon.Pokemon;
import pokeraidbot.domain.pokemon.PokemonRaidInfo;
import pokeraidbot.domain.pokemon.PokemonRepository;
import pokeraidbot.domain.raid.PokemonRaidStrategyService;
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.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import static pokeraidbot.Utils.*;
package pokeraidbot.infrastructure.botsupport.gymhuntr;




public class GymHuntrRaidEventListener implements EventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(GymHuntrRaidEventListener.class);

    private ServerConfigRepository serverConfigRepository;