@Mod(modid = Autotip.MOD_ID, name = Autotip.NAME, version = Autotip.VERSION, acceptedMinecraftVersions = Autotip.ACCEPTED_VERSIONS, clientSideOnly = true)
public class Autotip {

    public static final Logger LOGGER = LogManager.getLogger("Autotip");

    static final String MOD_ID = "autotip";
    static final String NAME = "Autotip";
    static final String VERSION = "3.0.1";
    static final String ACCEPTED_VERSIONS = "[1.8, 1.12.2]";

    private final List<Event> events = new ArrayList<>();
    private final List<CommandAbstract> commands = new ArrayList<>();

    private boolean initialized = false;

    private Minecraft minecraft;
    private MinecraftVersion mcVersion;
    private Version version;

    private Gson gson;

    private FileUtil fileUtil;
    private MessageUtil messageUtil;

    private Config config;
    private GlobalSettings globalSettings;
    private LocaleHolder localeHolder;

    private TaskManager taskManager;
    private SessionManager sessionManager;
    private MigrationManager migrationManager;
    private StatsManager statsManager;

    public boolean isInitialized() {
        return initialized;
    }

    public Minecraft getMinecraft() {
        return minecraft;
    }

    public GameProfile getGameProfile() {
        return minecraft.getSession().getProfile();
    }

    public MinecraftVersion getMcVersion() {
        return mcVersion;
    }

    public Version getVersion() {
        return version;
    }

    public Gson getGson() {
        return gson;
    }

    public FileUtil getFileUtil() {
        return fileUtil;
    }

    public MessageUtil getMessageUtil() {
        return messageUtil;
    }

    public Config getConfig() {
        return config;
    }

    public GlobalSettings getGlobalSettings() {
        return globalSettings;
    }

    public LocaleHolder getLocaleHolder() {
        return localeHolder;
    }

    public TaskManager getTaskManager() {
        return taskManager;
    }

    public SessionManager getSessionManager() {
        return sessionManager;
    }

    public MigrationManager getMigrationManager() {
        return migrationManager;
    }

    public StatsManager getStatsManager() {
        return statsManager;
    }

    @EventHandler
    public void preInit(FMLPreInitializationEvent event) {
        ErrorReport.setAutotip(this);
        RequestHandler.setAutotip(this);
        UniversalUtil.setAutotip(this);
    }

    @EventHandler
    public void init(FMLInitializationEvent event) {
        this.minecraft = Minecraft.getMinecraft();
        this.mcVersion = UniversalUtil.getMinecraftVersion();
        this.version = new Version(VERSION);

        this.messageUtil = new MessageUtil(this);
        this.registerEvents(new EventClientTick(this));
        this.taskManager = new TaskManager();
        this.taskManager.getExecutor().execute(this::setup);
    }

    private void setup() {
        try {
            this.fileUtil = new FileUtil(this);
            this.gson = new GsonBuilder()
                .registerTypeAdapter(Config.class, new ConfigCreator(this))
                .registerTypeAdapter(StatsDaily.class, new StatsDailyCreator(this))
                .setExclusionStrategies(new AnnotationExclusionStrategy())
                .setPrettyPrinting()
                .create();

            this.config = new Config(this);

            this.reloadGlobalSettings();
            this.reloadLocale();

            this.sessionManager = new SessionManager(this);
            this.statsManager = new StatsManager(this);
            this.migrationManager = new MigrationManager(this);

            this.fileUtil.createDirectories();
            this.config.load();
            this.taskManager.getExecutor().execute(() -> {
                this.migrationManager.migrateLegacyFiles();
            });

            this.registerEvents(
                new EventClientConnection(this),
                new EventChatReceived(this)
            );
            this.registerCommands(
                new CommandAutotip(this),
                new CommandLimbo(this)
            );
            Runtime.getRuntime().addShutdownHook(new Thread(sessionManager::logout));
            this.initialized = true;
        } catch (IOException e) {
            messageUtil.send("Autotip is disabled because it couldn't create the required files.");
            ErrorReport.reportException(e);
        } catch (IllegalStateException e) {
            messageUtil.send("Autotip is disabled because it couldn't connect to the API.");
            ErrorReport.reportException(e);
        }
    }

    public void reloadGlobalSettings() {
        SettingsReply reply = SettingsRequest.of(this).execute();
        if (!reply.isSuccess()) {
            throw new IllegalStateException("Connection error while fetching global settings");
        }
        this.globalSettings = reply.getSettings();
    }

    public void reloadLocale() {
        LocaleReply reply = LocaleRequest.of(this).execute();
        if (!reply.isSuccess()) {
            throw new IllegalStateException("Could not fetch locale");
        }
        this.localeHolder = reply.getLocaleHolder();
    }

    @SuppressWarnings("unchecked")
    public <T extends Event> T getEvent(Class<T> clazz) {
        return (T) events.stream()
            .filter(event -> event.getClass().equals(clazz))
            .findFirst()
            .orElse(null);
    }

    @SuppressWarnings("unchecked")
    public <T extends CommandAbstract> T getCommand(Class<T> clazz) {
        return (T) commands.stream()
            .filter(command -> command.getClass().equals(clazz))
            .findFirst()
            .orElse(null);
    }

    private void registerEvents(Event... events) {
        for (Event event : events) {
            MinecraftForge.EVENT_BUS.register(event); // 1.8.9 - 1.12.2
            FMLCommonHandler.instance().bus().register(event); // 1.8
            this.events.add(event);
        }
    }

    private void registerCommands(CommandAbstract... commands) {
        for (CommandAbstract command : commands) {
            ClientCommandHandler.instance.registerCommand(command);
            this.commands.add(command);
        }
    }

} public class SessionKey {

    private final String key;

    public SessionKey(String key) {
        this.key = key;
    }

    public String getKey() {
        return key;
    }

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

} public class KeepAliveReply extends Reply {

    public KeepAliveReply() {
    }

    public KeepAliveReply(boolean success) {
        super(success);
    }

    @Override
    public RequestType getRequestType() {
        return RequestType.KEEP_ALIVE;
    }

} public class LoginReply extends Reply {

    private SessionKey sessionKey;
    private long keepAliveRate;
    private long tipWaveRate;
    private long tipCycleRate;

    public LoginReply() {
    }

    public LoginReply(boolean success) {
        super(success);
    }

    public SessionKey getSessionKey() {
        return sessionKey;
    }

    public long getKeepAliveRate() {
        return keepAliveRate;
    }

    public long getTipWaveRate() {
        return tipWaveRate;
    }

    public long getTipCycleRate() {
        return tipCycleRate;
    }

    @Override
    public RequestType getRequestType() {
        return RequestType.LOGIN;
    }

} public class LogoutReply extends Reply {

    public LogoutReply() {
    }

    public LogoutReply(boolean success) {
        super(success);
    }

    @Override
    public RequestType getRequestType() {
        return RequestType.LOGOUT;
    }

} public class TipReply extends Reply {

    private List<Tip> tips;

    public TipReply() {
    }

    public TipReply(boolean success) {
        super(success);
    }

    private TipReply(List<Tip> tips) {
        this.tips = tips;
    }

    public static TipReply getDefault() {
        return new TipReply(Collections.singletonList(new Tip("all", null)));
    }

    public List<Tip> getTips() {
        return tips;
    }

    @Override
    public RequestType getRequestType() {
        return RequestType.TIP;
    }

    public static class Tip {

        private String gamemode;
        private String username;

        private Tip() {
        }

        private Tip(String gamemode, String username) {
            this.gamemode = gamemode;
            this.username = username;
        }

        public String getGamemode() {
            return gamemode;
        }

        public String getUsername() {
            return username != null ? username : "";
        }

        public String getAsCommand() {
            return "/tip " + this.toString();
        }

        @Override
        public String toString() {
            return (username != null && !username.isEmpty() ? username + " " : "") + gamemode;
        }

    }

} public static class Tip {

    private String gamemode;
    private String username;

    private Tip() {
    }

    private Tip(String gamemode, String username) {
        this.gamemode = gamemode;
        this.username = username;
    }

    public String getGamemode() {
        return gamemode;
    }

    public String getUsername() {
        return username != null ? username : "";
    }

    public String getAsCommand() {
        return "/tip " + this.toString();
    }

    @Override
    public String toString() {
        return (username != null && !username.isEmpty() ? username + " " : "") + gamemode;
    }

} public class KeepAliveRequest implements Request<KeepAliveReply> {

    private final SessionKey sessionKey;

    private KeepAliveRequest(SessionKey sessionKey) {
        this.sessionKey = sessionKey;
    }

    public static KeepAliveRequest of(SessionKey sessionKey) {
        return new KeepAliveRequest(sessionKey);
    }

    @Override
    public KeepAliveReply execute() {
        HttpUriRequest request = GetBuilder.of(this)
                .addParameter("key", this.sessionKey)
                .build();

        Optional<Reply> optional = RequestHandler.getReply(this, request.getURI());
        return optional
                .map(reply -> (KeepAliveReply) reply)
                .orElseGet(() -> new KeepAliveReply(false));
    }

    @Override
    public RequestType getType() {
        return RequestType.KEEP_ALIVE;
    }

} public class LoginRequest implements Request<LoginReply> {

    private final Autotip autotip;
    private final GameProfile profile;
    private final String hash;
    private final int tips;

    private LoginRequest(Autotip autotip, GameProfile profile, String hash, int tips) {
        this.autotip = autotip;
        this.profile = profile;
        this.hash = hash;
        this.tips = tips;
    }

    public static LoginRequest of(Autotip autotip, GameProfile profile, String hash, int tips) {
        return new LoginRequest(autotip, profile, hash, tips);
    }

    @Override
    public LoginReply execute() {
        HttpUriRequest request = GetBuilder.of(this)
                .addParameter("username", this.profile.getName())
                .addParameter("uuid", this.profile.getId().toString().replace("-", ""))
                .addParameter("tips", this.tips)
                .addParameter("v", this.autotip.getVersion())
                .addParameter("mc", this.autotip.getMcVersion())
                .addParameter("os", System.getProperty("os.name"))
                .addParameter("hash", this.hash)
                .build();

        Optional<Reply> optional = RequestHandler.getReply(this, request.getURI());
        return optional
                .map(reply -> (LoginReply) reply)
                .orElseGet(() -> new LoginReply(false));
    }

    @Override
    public RequestType getType() {
        return RequestType.LOGIN;
    }

} public class LogoutRequest implements Request<LogoutReply> {

    private final SessionKey sessionKey;

    private LogoutRequest(SessionKey sessionKey) {
        this.sessionKey = sessionKey;
    }

    public static LogoutRequest of(SessionKey sessionKey) {
        return new LogoutRequest(sessionKey);
    }

    @Override
    public LogoutReply execute() {
        HttpUriRequest request = GetBuilder.of(this)
                .addParameter("key", this.sessionKey)
                .build();

        Optional<Reply> optional = RequestHandler.getReply(this, request.getURI());
        return optional
                .map(reply -> (LogoutReply) reply)
                .orElseGet(() -> new LogoutReply(false));
    }

    @Override
    public RequestType getType() {
        return RequestType.LOGOUT;
    }

} public class TipRequest implements Request<TipReply> {

    private final SessionKey sessionKey;

    private TipRequest(SessionKey sessionKey) {
        this.sessionKey = sessionKey;
    }

    public static TipRequest of(SessionKey sessionKey) {
        return new TipRequest(sessionKey);
    }

    @Override
    public TipReply execute() {
        HttpUriRequest request = GetBuilder.of(this)
                .addParameter("key", this.sessionKey)
                .build();

        Optional<Reply> optional = RequestHandler.getReply(this, request.getURI());
        return optional
                .map(reply -> (TipReply) reply)
                .orElseGet(TipReply::getDefault);
    }

    @Override
    public RequestType getType() {
        return RequestType.TIP;
    }

} public class MessageUtil {

    private static final String PREFIX = "&6A&eT &8> &7";

    private final Autotip autotip;

    private final Queue<String> chatQueue = Queues.newConcurrentLinkedQueue();
    private final Queue<String> cmdQueue = Queues.newConcurrentLinkedQueue();

    public MessageUtil(Autotip autotip) {
        this.autotip = autotip;
    }

    public ChatComponentBuilder getBuilder(String text, Object... params) {
        return this.getBuilder(true, text, params);
    }

    public ChatComponentBuilder getBuilder(boolean prefix, String text, Object... params) {
        return ChatComponentBuilder.of(prefix, text, params);
    }

    public KeyHelper getKeyHelper(String rootKey) {
        return new KeyHelper(this, rootKey);
    }

    public void sendKey(String key, Object... params) {
        this.send(this.getKey(key), params);
    }

    public String getKey(String key) {
        return autotip.getLocaleHolder().getKey(key);
    }

    public void send(String msg, Object... params) {
        this.sendRaw(PREFIX + msg, params);
    }

    public void send(String msg, String url, String hoverText, Object... params) {
        UniversalUtil.addChatMessage(StringUtil.params(PREFIX + msg, params), url,
                StringUtil.format(hoverText));
    }

    public void separator() {
        this.sendRaw("&6&m&l----------------------------------");
    }

    public void sendRaw(String msg, Object... params) {
        msg = StringUtil.params(msg, params);
        if (this.isPlayerLoaded()) {
            this.flushQueues();
            UniversalUtil.addChatMessage(msg);
        } else {
            chatQueue.add(msg);
            Autotip.LOGGER.info("Queued chat message: " + msg);
        }
    }

    public void sendCommand(String command) {
        if (this.isPlayerLoaded()) {
            this.flushQueues();
            autotip.getMinecraft().thePlayer.sendChatMessage(command);
        } else {
            cmdQueue.add(command);
            Autotip.LOGGER.info("Queued command: " + command);
        }
    }

    public void flushQueues() {
        if (this.isPlayerLoaded()) {
            while (!chatQueue.isEmpty()) {
                this.sendRaw(chatQueue.poll());
            }
            while (!cmdQueue.isEmpty()) {
                this.sendCommand(cmdQueue.poll());
            }
        }
    }

    public void clearQueues() {
        chatQueue.clear();
        cmdQueue.clear();
    }

    private boolean isPlayerLoaded() {
        Minecraft minecraft = autotip.getMinecraft();
        return minecraft != null && minecraft.thePlayer != null;
    }

} public enum TaskType {
    LOGIN, KEEP_ALIVE, TIP_WAVE, TIP_CYCLE, LOGOUT
} public class EventClientConnection implements Event {

    private static Field HEADER_FIELD;

    static {
        try {
            HEADER_FIELD = ReflectionUtil
                    .findField(ReflectionUtil.findClazz("net.labymod.core_implementation.mc18.gui.ModPlayerTabOverlay"),
                            "header");
        } catch (Exception e) {
            HEADER_FIELD = ReflectionUtil
                    .findField(GuiPlayerTabOverlay.class, "field_175256_i", "header");
        }
    }

    private final Autotip autotip;
    private final String hypixelHeader;

    private String serverIp;
    private long lastLogin;

    public EventClientConnection(Autotip autotip) {
        this.autotip = autotip;
        this.hypixelHeader = autotip.getGlobalSettings().getHypixelHeader();
    }

    public String getServerIp() {
        return serverIp;
    }

    public long getLastLogin() {
        return lastLogin;
    }

    public Object getHeader() {
        try {
            return HEADER_FIELD.get(autotip.getMinecraft().ingameGUI.getTabList());
        } catch (IllegalAccessException | NullPointerException e) {
            ErrorReport.reportException(e);
            return null;
        }
    }

    private void resetHeader() {
        try {
            HEADER_FIELD.set(autotip.getMinecraft().ingameGUI.getTabList(), null);
        } catch (IllegalAccessException e) {
            ErrorReport.reportException(e);
        }
    }

    @SubscribeEvent
    public void playerLoggedIn(ClientConnectedToServerEvent event) {
        TaskManager taskManager = autotip.getTaskManager();
        SessionManager manager = autotip.getSessionManager();

        autotip.getMessageUtil().clearQueues();

        this.serverIp = UniversalUtil.getRemoteAddress(event).toString().toLowerCase();
        this.lastLogin = System.currentTimeMillis();

        taskManager.getExecutor().execute(() -> {
            Object header;
            int attempts = 0;
            while ((header = this.getHeader()) == null) {
                if (attempts > 15) {
                    return;
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ignored) {
                }
                attempts++;
            }

            if (UniversalUtil.getUnformattedText(header).equals(hypixelHeader)) {
                manager.setOnHypixel(true);
                if (autotip.getConfig().isEnabled()) {
                    taskManager.executeTask(TaskType.LOGIN, manager::login);
                    taskManager.schedule(manager::checkVersions, 5);
                }
            } else {
                manager.setOnHypixel(false);
            }
        });
    }

    @SubscribeEvent
    public void playerLoggedOut(ClientDisconnectionFromServerEvent event) {
        TaskManager taskManager = autotip.getTaskManager();
        SessionManager manager = autotip.getSessionManager();
        manager.setOnHypixel(false);
        taskManager.executeTask(TaskType.LOGOUT, manager::logout);
        this.resetHeader();
    }

} public class StatsRange extends Stats {

    private final LocalDate start;
    private final LocalDate end;

    public StatsRange(Autotip autotip, LocalDate start, LocalDate end) {
        super(autotip);
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start date is after end date");
        }
        this.start = start;
        this.end = end;
    }

    public LocalDate getStart() {
        return start;
    }

    public LocalDate getEnd() {
        return end;
    }

    public String getStartString() {
        return DATE_FORMATTER.format(start);
    }

    public String getEndString() {
        return DATE_FORMATTER.format(end);
    }


    @Override
    public StatsRange merge(final Stats that) {
        if (that instanceof StatsRange) {
            throw new UnsupportedOperationException("Cannot merge StatsRange with StatsRange");
        }
        if (!(that instanceof StatsDaily)) {
            throw new IllegalArgumentException("Cannot merge with StatsRange");
        }
        LocalDate date = ((StatsDaily) that).date;
        if (date.isBefore(start) || date.isAfter(end)) {
            throw new IllegalArgumentException("Date is not in range");
        }
        super.merge(that);
        return this;
    }

} public class ErrorReport {

    private static Autotip autotip;

    public static void setAutotip(Autotip autotip) {
        ErrorReport.autotip = autotip;
    }

    public static void reportException(Throwable t) {
        Autotip.LOGGER.error(t.getMessage(), t);
        try {
            URL url = new URL("https://api.autotip.pro/error_report.php");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            conn.setRequestMethod("POST");
            conn.setDoOutput(true);

            JsonObjectBuilder builder = JsonObjectBuilder.newBuilder()
                    .addString("username", autotip.getGameProfile().getName())
                    .addString("uuid", autotip.getGameProfile().getId())
                    .addString("v", autotip.getVersion())
                    .addString("mc", autotip.getMcVersion())
                    .addString("os", System.getProperty("os.name"))
                    .addString("forge", ForgeVersion.getVersion())
                    .addString("stackTrace", ExceptionUtils.getStackTrace(t))
                    .addNumber("time", System.currentTimeMillis());

            if (autotip.isInitialized()) {
                EventClientConnection event = autotip.getEvent(EventClientConnection.class);
                builder.addString("sessionKey", autotip.getSessionManager().getKey())
                        .addString("serverIp", event.getServerIp());
            }

            byte[] jsonBytes = builder.build().toString().getBytes(StandardCharsets.UTF_8);

            conn.setFixedLengthStreamingMode(jsonBytes.length);
            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
            conn.setRequestProperty("User-Agent", "Autotip v" + autotip.getVersion());
            conn.connect();

            try (OutputStream out = conn.getOutputStream()) {
                out.write(jsonBytes);
            }

            InputStream input;
            if (conn.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST) {
                input = conn.getInputStream();
            } else {
                input = conn.getErrorStream();
            }
            String json = IOUtils.toString(input, StandardCharsets.UTF_8);
            Autotip.LOGGER.info("Error JSON: " + json);

        } catch (IOException e) {
            // Hmm... what would happen if I were to report this one?
            e.printStackTrace();
        }
    }

    private static class JsonObjectBuilder {

        private final JsonObject obj;

        private JsonObjectBuilder(JsonObject obj) {
            this.obj = obj;
        }

        static JsonObjectBuilder newBuilder() {
            return new JsonObjectBuilder(new JsonObject());
        }

        JsonObjectBuilder addString(String property, Object value) {
            obj.addProperty(property, String.valueOf(value));
            return this;
        }

        JsonObjectBuilder addNumber(String property, Number value) {
            obj.addProperty(property, value);
            return this;
        }

        JsonObject build() {
            return obj;
        }

    }

} public class HashUtil {

    private static final SecureRandom RANDOM = new SecureRandom();

    public static String getNextSalt() {
        return new BigInteger(130, RANDOM).toString(32);
    }

    public static String hash(String str) {
        try {
            byte[] digest = digest(str, "SHA-1");
            return new BigInteger(digest).toString(16);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static byte[] digest(String str, String algorithm) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance(algorithm);
        byte[] strBytes = str.getBytes(StandardCharsets.UTF_8);
        return md.digest(strBytes);
    }

} public class VersionInfo {

    private Version version;
    private Severity severity;
    private List<String> changelog;

    public VersionInfo(Version version, Severity severity, String... changelog) {
        this(version, severity, Arrays.asList(changelog));
    }

    public VersionInfo(Version version, Severity severity, List<String> changelog) {
        this.version = version;
        this.severity = severity;
        this.changelog = changelog;
    }

    public Version getVersion() {
        return version;
    }

    public Severity getSeverity() {
        return severity;
    }

    public List<String> getChangelog() {
        return changelog;
    }

    public enum Severity {
        OPTIONAL, ADVISED, CRITICAL;

        public String toColoredString() {
            ChatColor color;
            switch (this) {
                default:
                case OPTIONAL:
                    color = ChatColor.GREEN;
                    break;
                case ADVISED:
                    color = ChatColor.YELLOW;
                    break;
                case CRITICAL:
                    color = ChatColor.RED;
                    break;
            }
            return color + this.toString();
        }
    }

}
import com.google.gson.JsonObject;
import com.mojang.authlib.GameProfile;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import me.semx11.autotip.Autotip;
import me.semx11.autotip.api.SessionKey;
import me.semx11.autotip.api.reply.impl.KeepAliveReply;
import me.semx11.autotip.api.reply.impl.LoginReply;
import me.semx11.autotip.api.reply.impl.LogoutReply;
import me.semx11.autotip.api.reply.impl.TipReply;
import me.semx11.autotip.api.reply.impl.TipReply.Tip;
import me.semx11.autotip.api.request.impl.KeepAliveRequest;
import me.semx11.autotip.api.request.impl.LoginRequest;
import me.semx11.autotip.api.request.impl.LogoutRequest;
import me.semx11.autotip.api.request.impl.TipRequest;
import me.semx11.autotip.chat.MessageUtil;
import me.semx11.autotip.core.TaskManager.TaskType;
import me.semx11.autotip.event.impl.EventClientConnection;
import me.semx11.autotip.stats.StatsRange;
import me.semx11.autotip.util.ErrorReport;
import me.semx11.autotip.util.HashUtil;
import me.semx11.autotip.util.VersionInfo;
import net.minecraft.util.Session;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
package me.semx11.autotip.core;


public class SessionManager {

    private final Autotip autotip;
    private final MessageUtil messageUtil;
    private final TaskManager taskManager;

    private final Queue<Tip> tipQueue = new ConcurrentLinkedQueue<>();

    private LoginReply reply;
    private SessionKey sessionKey = null;

    private boolean onHypixel = false;
    private boolean loggedIn = false;

    private long lastTipWave;
    private long nextTipWave;

    public SessionManager(Autotip autotip) {
        this.autotip = autotip;
        this.messageUtil = autotip.getMessageUtil();
        this.taskManager = autotip.getTaskManager();
    }

    public SessionKey getKey() {
        return sessionKey;
    }

    public boolean hasKey() {
        return sessionKey != null;
    }

    public boolean isOnHypixel() {
        return onHypixel;
    }

    public void setOnHypixel(boolean onHypixel) {
        this.onHypixel = onHypixel;
    }

    public boolean isLoggedIn() {
        return loggedIn;
    }

    public long getLastTipWave() {
        return lastTipWave;
    }

    public long getNextTipWave() {
        return nextTipWave;
    }

    public void checkVersions() {
        List<VersionInfo> versions = autotip.getGlobalSettings()
                .getHigherVersionInfo(autotip.getVersion());
        if (versions.size() > 0) {
            messageUtil.separator();
            messageUtil.getKeyHelper("update")
                    .withKey("message", context -> context.getBuilder()
                            .setUrl(context.getKey("url"))
                            .setHover(context.getKey("hover"))
                            .send())
                    .sendKey("changelogHeader");
            versions.forEach(info -> {
                messageUtil.sendKey("update.version", info.getVersion(),
                        info.getSeverity().toColoredString());
                info.getChangelog().forEach(s -> messageUtil.sendKey("update.logEntry", s));
            });
            messageUtil.separator();
        }
    }

    public void login() {
        Session session = autotip.getMinecraft().getSession();
        GameProfile profile = session.getProfile();

        String uuid = profile.getId().toString().replace("-", "");
        String serverHash = HashUtil.hash(uuid + HashUtil.getNextSalt());

        int statusCode = this.authenticate(session.getToken(), uuid, serverHash);
        if (statusCode / 100 != 2) {
            messageUtil.send("&cError {} during authentication: Session servers down?", statusCode);
            return;
        }

        StatsRange all = autotip.getStatsManager().getAll();
        LoginRequest request = LoginRequest.of(autotip, profile, serverHash, all.getTipsTotalInt());

        long lastLogin = autotip.getEvent(EventClientConnection.class).getLastLogin();
        long delay = lastLogin + 5000 - System.currentTimeMillis();
        delay /= 1000;

        this.reply = taskManager.scheduleAndAwait(request::execute, (delay < 1) ? 1 : delay);
        if (reply == null || !reply.isSuccess()) {
            messageUtil.send("&cError during login: {}", reply == null ? "null" : reply.getCause());
            return;
        }

        this.sessionKey = reply.getSessionKey();

        this.loggedIn = true;

        long keepAlive = reply.getKeepAliveRate();
        long tipWave = reply.getTipWaveRate();

        taskManager.addRepeatingTask(TaskType.KEEP_ALIVE, this::keepAlive, keepAlive, keepAlive);
        taskManager.addRepeatingTask(TaskType.TIP_WAVE, this::tipWave, 0, tipWave);
    }

    public void logout() {
        if (!loggedIn) {
            return;
        }
        LogoutReply reply = LogoutRequest.of(sessionKey).execute();
        if (!reply.isSuccess()) {
            Autotip.LOGGER.warn("Error during logout: {}", reply.getCause());
        }

        this.loggedIn = false;
        this.sessionKey = null;

        taskManager.cancelTask(TaskType.KEEP_ALIVE);
        tipQueue.clear();
    }

    private void keepAlive() {
        if (!onHypixel || !loggedIn) {
            taskManager.cancelTask(TaskType.KEEP_ALIVE);
            return;
        }
        KeepAliveReply r = KeepAliveRequest.of(sessionKey).execute();