package com.leagueofnewbs.glitchify;

import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.util.Log;

import static de.robv.android.xposed.XposedHelpers.*;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.IXposedHookZygoteInit;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XSharedPreferences;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.Locale;

import javax.net.ssl.HttpsURLConnection;

public class Main implements IXposedHookLoadPackage, IXposedHookZygoteInit {

    private static XSharedPreferences pref;
    private Preferences preferences;
    private final ColorHelper colorHelper = ColorHelper.getInstance();
    private final Hashtable<String, String> ffzRoomEmotes = new Hashtable<>();
    private final Hashtable<String, String> ffzGlobalEmotes = new Hashtable<>();
    private final Hashtable<String, Hashtable<String, Object>> ffzBadges = new Hashtable<>();
    private final Hashtable<String, String> bttvRoomEmotes = new Hashtable<>();
    private final Hashtable<String, String> bttvGlobalEmotes = new Hashtable<>();
    private final Hashtable<String, Hashtable<String, Object>> bttvBadges = new Hashtable<>();
    private static Object customModBadgeImage;
    private static final String ffzAPIURL = "https://api.frankerfacez.com/v1/";
    private static final String bttvAPIURL = "https://api.betterttv.net/3/cached/";
    private static final String bttvUrlTemplate = "https://cdn.betterttv.net/emote/{{id}}/{{image}}";
    private static final String logTag = "Glitchify";

    public void initZygote(IXposedHookZygoteInit.StartupParam startupParam) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            pref = new XSharedPreferences(new File("/data/user_de/0/com.leagueofnewbs.glitchify/shared_prefs/preferences.xml"));
        } else {
            //noinspection ConstantConditions
            pref = new XSharedPreferences(Main.class.getPackage().getName(), "preferences");
        }
    }

    @SuppressWarnings("RedundantThrows")
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("tv.twitch.android.app") || !lpparam.isFirstApplication) {
            return;
        }

        preferences = new Preferences(pref);

        // Get all global info that we can all at once
        // FFZ/BTTV global emotes, global twitch badges, and FFZ mod badge
        Thread globalThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (preferences.ffzEmotes()) {
                        getFFZGlobalEmotes();
                    }
                } catch (Exception e) {
                    printException(e, "Error fetching global FFZ emotes > ");
                }
                try {
                    if (preferences.bttvEmotes()) {
                        getBTTVGlobalEmotes();
                    }
                } catch (Exception e) {
                    printException(e, "Error fetching global BTTV emotes > ");
                }
                try {
                    if (preferences.ffzBadges()) {
                        getFFZBadges();
                    }
                } catch (Exception e) {
                    printException(e, "Error fetching global FFZ badges > ");
                }
                try {
                    if (preferences.bttvBadges()) {
                        getBTTVBadges();
                    }
                } catch (Exception e) {
                    printException(e, "Error fetching global BTTV badges > ");
                }
            }
        });
        globalThread.start();


        // These are all the different class definitions that are needed in the function hooking
        final Class<?> chatControllerClass = findClass("tv.twitch.android.sdk.z", lpparam.classLoader);
        final Class<?> chatUpdaterClass = findClass("tv.twitch.android.sdk.z$f", lpparam.classLoader);
        final Class<?> chatViewPresenterClass = findClass("tv.twitch.a.k.g.n", lpparam.classLoader);
        final Class<?> messageRecyclerItemClass = findClass("tv.twitch.android.adapters.a.b", lpparam.classLoader);
        final Class<?> channelChatAdapterClass = findClass("tv.twitch.a.k.g.n0.a", lpparam.classLoader);
        final Class<?> chatUtilClass = findClass("tv.twitch.a.k.g.r1.g", lpparam.classLoader);
        final Class<?> deletedMessageClickableSpanClass = findClass("tv.twitch.a.k.g.r1.l", lpparam.classLoader);
        final Class<?> systemMessageTypeClass = findClass("tv.twitch.a.k.g.n0.g", lpparam.classLoader);
        final Class<?> chatMessageFactoryClass = findClass("tv.twitch.a.k.g.e1.a", lpparam.classLoader);
        final Class<?> clickableUsernameSpanClass = findClass("tv.twitch.a.k.g.r1.j", lpparam.classLoader);
        final Class<?> iClickableUsernameSpanListenerClass = findClass("tv.twitch.a.k.g.t0.a", lpparam.classLoader);
        final Class<?> twitchUrlSpanClickListenerInterfaceClass = findClass("tv.twitch.a.k.c0.b.s.g", lpparam.classLoader);
        final Class<?> censoredMessageTrackingInfoClass = findClass("tv.twitch.a.k.g.p1.c", lpparam.classLoader);
        final Class<?> webViewSourceEnumClass = findClass("tv.twitch.android.models.webview.WebViewSource", lpparam.classLoader);
        final Class<?> chatMessageInterfaceClass = findClass("tv.twitch.a.k.g.g", lpparam.classLoader);
        final Class<?> chatBadgeImageClass = findClass("tv.twitch.chat.ChatBadgeImage", lpparam.classLoader);
        final Class<?> bitsTokenClass = findClass("tv.twitch.android.models.chat.MessageToken$BitsToken", lpparam.classLoader);
        final Class<?> cheermotesHelperClass = findClass("tv.twitch.a.k.d.a0.h", lpparam.classLoader);
        final Class<?> chommentModelDelegateClass = findClass("tv.twitch.a.k.g.u0.c", lpparam.classLoader);
        final Class<?> EventDispatcherClass = findClass("tv.twitch.android.core.mvp.viewdelegate.EventDispatcher", lpparam.classLoader);
        final Class<?> channelInfoClass = findClass("tv.twitch.android.models.channel.ChannelInfo", lpparam.classLoader);
        final Class<?> streamTypeClass = findClass("tv.twitch.android.models.streams.StreamType", lpparam.classLoader);
        //noinspection unchecked
        final Class<? extends Enum> mediaSpanClass = (Class<? extends Enum>) findClass("tv.twitch.a.k.c0.b.s.d", lpparam.classLoader);
        final Class<?> vodPlayerPresenterClass = findClass("tv.twitch.a.k.v.j0.w", lpparam.classLoader);
        final Class<?> vodModelClass = findClass("tv.twitch.android.models.videos.VodModel", lpparam.classLoader);
        final Class<?> videoAdManagerClass = findClass("tv.twitch.android.player.ads.VideoAdManager", lpparam.classLoader);
        // Updated combined bits insertion object field to find bits helper in ChatMessageFactory

        // This is called when a vod chat widget gets a channel name attached to it
        // It sets up all the channel specific stuff (bttv/ffz emotes, etc)
        findAndHookMethod(vodPlayerPresenterClass, "a", vodModelClass, int.class, String.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                final String channelName = (String) callMethod(param.args[0], "getChannelName");
                final int channelId = (int) callMethod(param.args[0], "getBroadcasterId");
                getRoomEmotes(channelName, channelId);
            }
        });

        // This is called when a live chat widget gets a channel name attached to it
        // It sets up all the channel specific stuff (bttv/ffz emotes, etc)
        findAndHookMethod(chatViewPresenterClass, "a", channelInfoClass, String.class, streamTypeClass, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                final String channelName = (String) callMethod(param.args[0], "getName");
                final int channelId = (int) callMethod(param.args[0], "getId");
                getRoomEmotes(channelName, channelId);
            }
        });

        // This is what actually goes through and strikes out the messages
        // If show deleted is false this will replace with <message deleted>
        findAndHookMethod(chatUtilClass, "a", Spanned.class, String.class, deletedMessageClickableSpanClass, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (preferences.showDeletedMessages()) {
                    Spanned messageSpan = (Spanned) param.args[0];
                    Object[] spans = messageSpan.getSpans(0, messageSpan.length(), clickableUsernameSpanClass);
                    if ((spans.length == 0 ? 1 : null) != null) {
                        param.setResult(null);
                        return;
                    }

                    int spanEnd = messageSpan.getSpanEnd(spans[0]);
                    int length = 2 + spanEnd;
                    if (length < messageSpan.length() && messageSpan.subSequence(spanEnd, length).toString().equals(": ")) {
                        spanEnd = length;
                    }
                    SpannableStringBuilder ssb = new SpannableStringBuilder(messageSpan, 0, spanEnd);
                    SpannableStringBuilder ssb2 = new SpannableStringBuilder(messageSpan, spanEnd, messageSpan.length());
                    ssb.append(ssb2);
                    ssb.setSpan(new StrikethroughSpan(), ssb.length() - ssb2.length(), ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    param.setResult(ssb);
                }
            }
        });

        // Add timestamps to the beginning of every message
        findAndHookConstructor(messageRecyclerItemClass, "android.content.Context", String.class, int.class, String.class, String.class, int.class, Spanned.class, systemMessageTypeClass, float.class, int.class, float.class, boolean.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (preferences.showTimeStamps()) {
                    SimpleDateFormat formatter = new SimpleDateFormat("h:mm ", Locale.US);
                    SpannableString dateString = SpannableString.valueOf(formatter.format(new Date()));
                    dateString.setSpan(new RelativeSizeSpan(0.75f), 0, dateString.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    CharSequence messageSpan = (CharSequence) param.args[6];
                    SpannableStringBuilder message = new SpannableStringBuilder(dateString);
                    message.append(messageSpan);
                    param.args[6] = SpannedString.valueOf(message);
                }
            }
        });

        // Override complete chat clears
        findAndHookMethod(chatUpdaterClass, "chatChannelMessagesCleared", int.class, int.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (preferences.preventChatClear()) {
                    param.setResult(null);
                }
            }
        });
        XposedBridge.hookAllMethods(chatUpdaterClass, "chatChannelModNoticeClearChat", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (preferences.preventChatClear()) {
                    param.setResult(null);
                }
            }
        });

        // Prevent overriding of chat history length
        findAndHookConstructor(channelChatAdapterClass, int.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                param.args[0] = preferences.chatScrollbackLength();
            }
        });

        // Inject all badges and emotes into the finished message
        findAndHookMethod(chatMessageFactoryClass, "a", chatMessageInterfaceClass, boolean.class, boolean.class, boolean.class, int.class, int.class, iClickableUsernameSpanListenerClass, twitchUrlSpanClickListenerInterfaceClass, webViewSourceEnumClass, String.class, boolean.class, censoredMessageTrackingInfoClass, Integer.class, EventDispatcherClass, new XC_MethodHook() {
            @Override
            protected void  beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (preferences.bitsCombine() && !chommentModelDelegateClass.isInstance(param.args[0])) {
                    setAdditionalInstanceField(param.thisObject, "allowBitInsertion", false);
                }
                if (preferences.colorAdjust()) {
                    Integer color = (Integer) param.args[4];
                    Integer newColor = colorHelper.maybeBrighten(color, preferences.darkMode());
                    param.args[4] = newColor;
                }
            }

            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                SpannableStringBuilder msg = new SpannableStringBuilder((SpannedString) param.getResult());

                if (preferences.ffzBadges()) {
                    msg = injectBadges(param, mediaSpanClass, msg, ffzBadges);
                }
                if (preferences.bttvBadges()) {
                    msg = injectBadges(param, mediaSpanClass, msg, bttvBadges);
                }
                if (preferences.ffzEmotes()) {
                    msg = injectEmotes(param, mediaSpanClass, msg, ffzGlobalEmotes);
                    msg = injectEmotes(param, mediaSpanClass, msg, ffzRoomEmotes);
                }
                if (preferences.bttvEmotes()) {
                    msg = injectEmotes(param, mediaSpanClass, msg, bttvGlobalEmotes);
                    msg = injectEmotes(param, mediaSpanClass, msg, bttvRoomEmotes);
                }

                if (preferences.bitsCombine() && !chommentModelDelegateClass.isInstance(param.args[0])) {
                    setAdditionalInstanceField(param.thisObject, "allowBitInsertion", true);
                    Object chatMessageInfo = getObjectField(param.args[0], "a");
                    int numBits = getIntField(chatMessageInfo, "numBitsSent");
                    if (numBits > 0) {
                        Object bit = newInstance(bitsTokenClass, "cheer", numBits);
                        SpannableString bitString = (SpannableString) callMethod(param.thisObject, "a", bit, getObjectField(param.thisObject, "b"));
                        if (bitString != null) {
                            msg.append(" ");
                            msg.append(bitString);
                        }
                    }
                }
                param.setResult(SpannableString.valueOf(msg));
            }
        });

        // Stop bits from being put into chat by the message factory
        findAndHookMethod(chatMessageFactoryClass, "a", bitsTokenClass, cheermotesHelperClass, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (!((Boolean) getAdditionalInstanceField(param.thisObject, "allowBitInsertion"))) {
                    param.setResult(null);
                }
            }
        });

        // Return null for any hidden badges, for some reason this works and I'm not going to complain because it's much easier this way
        // If custom mod badge, return a customized ChatBadgeImage instance with our url for mod badge
        // Whenever we leave the chat, return to using the default
        findAndHookMethod(chatControllerClass, "a", int.class, String.class, String.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                String badgeName = (String) param.args[1];
                if (preferences.hiddenBadges().contains(badgeName)) {
                    param.setResult(null);
                }
                if (preferences.ffzModBadge() && !preferences.ffzModBadgeURL().equals("") && badgeName.equals("moderator")) {
                    // Set and save a badge image to be reused for all messages
                    if (customModBadgeImage == null || !getObjectField(customModBadgeImage, "url").equals(preferences.ffzModBadgeURL())) {
                        customModBadgeImage = newInstance(chatBadgeImageClass);
                        setObjectField(customModBadgeImage, "url", preferences.ffzModBadgeURL());
                        setFloatField(customModBadgeImage, "scale", preferences.ffzModBadgeScale());
                    }
                    param.setResult(customModBadgeImage);
                }
            }
        });

        XposedBridge.hookAllMethods(videoAdManagerClass, "requestAds", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (preferences.hideAds()) {
                    param.setResult(null);
                }
            }
        });
    }

    private SpannableStringBuilder injectBadges(XC_MethodHook.MethodHookParam param, Class mediaSpanClass, SpannableStringBuilder chatMsg, Hashtable customBadges) {
        String chatSender = (String) callMethod(param.args[0], "getDisplayName");
        int location = chatMsg.toString().indexOf(chatSender);
        if (location == -1) { return chatMsg; }

        int badgeCount;
        if (location == 0) {
            badgeCount = location;
        } else {
            badgeCount = chatMsg.toString().substring(0, location - 1).split(" ").length;
        }

        for (Object key : customBadges.keySet()) {
            if (badgeCount >= 3) {
                // Already at 3 badges, anymore will clog up chat box
                return chatMsg;
            }
            String keyString = (String) key;
            if (preferences.hiddenBadges().contains(keyString)) {
                continue;
            }
            if (!((ArrayList) ((Hashtable) customBadges.get(keyString)).get("users")).contains(chatSender)) {
                continue;
            }
            String url = (String) ((Hashtable) customBadges.get(keyString)).get("image");
            SpannableString badgeSpan = (SpannableString) callMethod(param.thisObject, "a", param.thisObject, url, Enum.valueOf(mediaSpanClass, "Badge"), keyString + " ", null, true, 8, null);
            chatMsg.insert(location, badgeSpan);
            location += badgeSpan.length();
            badgeCount++;
        }
        return chatMsg;
    }

    private SpannableStringBuilder injectEmotes(XC_MethodHook.MethodHookParam param, Class mediaSpanClass, SpannableStringBuilder chatMsg, Hashtable customEmoteHash) {
        String chatSender = (String) callMethod(param.args[0], "getDisplayName");
        for (Object key : customEmoteHash.keySet()) {
            String keyString = (String) key;
            int location = chatMsg.toString().indexOf(chatSender);
            if (location == -1) { return chatMsg; }

            location++;
            int keyLength = keyString.length();
            while ((location = chatMsg.toString().indexOf(keyString, location)) != -1) {
                try {
                    if (chatMsg.charAt(location - 1) != ' ' || chatMsg.charAt(location + keyLength) != ' ') {
                        ++location;
                        continue;
                    }
                } catch(IndexOutOfBoundsException e) {
                    // End of line reached
                }

                String url = customEmoteHash.get(keyString).toString();
                SpannableString emoteSpan = (SpannableString) callMethod(param.thisObject, "a", param.thisObject, url, Enum.valueOf(mediaSpanClass, "Emote"), keyString, null, false, 24, null);
                chatMsg.replace(location, location + keyLength, emoteSpan);
                location += keyString.length();
            }
        }

        return chatMsg;
    }

    private void getRoomEmotes(final String channelName, final int channelId) {
        Thread roomThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (preferences.ffzEmotes()) {
                        getFFZRoomEmotes(channelName);
                    }
                } catch (Exception e) {
                    printException(e, "Error fetching FFZ emotes for " + channelName + " > ");
                }
                try {
                    if (preferences.bttvEmotes()) {
                        getBTTVRoomEmotes(channelId);
                    }
                } catch (Exception e) {
                    printException(e, "Error fetching BTTV emotes for " + channelName + " > ");
                }
            }
        });
        roomThread.start();
    }

    private void getFFZRoomEmotes(String channel) throws Exception {
        ffzRoomEmotes.clear();
        URL roomURL = new URL(ffzAPIURL + "room/" + channel);
        JSONObject roomEmotes = getJSON(roomURL).jsonAsObject();
        try {
            int status = roomEmotes.getInt("status");
            if (status == 404) {
                preferences.ffzModBadgeURL("");
                return;
            }
        } catch (JSONException e) {
            // Required to compile
        }
        int set = roomEmotes.getJSONObject("room").getInt("set");
        if (roomEmotes.getJSONObject("room").isNull("moderator_badge")) {
            preferences.ffzModBadgeURL("");
        } else {
            JSONObject modURLs = roomEmotes.getJSONObject("room").getJSONObject("mod_urls");
            String url = modURLs.getString("1");
            if (modURLs.has("2")) {
                url = modURLs.getString("2");
                preferences.ffzModBadgeScale(2);
            }
            preferences.ffzModBadgeURL("https:" + url + "/solid");
        }
        JSONArray roomEmoteArray = roomEmotes.getJSONObject("sets").getJSONObject(Integer.toString(set)).getJSONArray("emoticons");
        for (int i = 0; i < roomEmoteArray.length(); ++i) {
            String emoteName = roomEmoteArray.getJSONObject(i).getString("name");
            String emoteURL = roomEmoteArray.getJSONObject(i).getJSONObject("urls").getString("1");
            ffzRoomEmotes.put(emoteName, "https:" + emoteURL);
        }
    }

    private void getFFZGlobalEmotes() throws Exception {
        URL globalURL = new URL(ffzAPIURL + "set/global");
        JSONObject globalEmotes = getJSON(globalURL).jsonAsObject();
        JSONArray setsArray = globalEmotes.getJSONArray("default_sets");
        for (int i = 0; i < setsArray.length(); ++i) {
            int set = setsArray.getInt(i);
            JSONArray globalEmotesArray = globalEmotes.getJSONObject("sets").getJSONObject(Integer.toString(set)).getJSONArray("emoticons");
            for (int j = 0; j < globalEmotesArray.length(); ++j) {
                String emoteName = globalEmotesArray.getJSONObject(j).getString("name");
                String emoteURL = globalEmotesArray.getJSONObject(j).getJSONObject("urls").getString("1");
                ffzGlobalEmotes.put(emoteName, "https:" + emoteURL);
            }
        }
    }

    @SuppressWarnings({"unchecked", "ConstantConditions"})
    private void getFFZBadges() throws Exception {
        URL badgeURL = new URL(ffzAPIURL + "badges");
        JSONObject badges = getJSON(badgeURL).jsonAsObject();
        JSONArray badgesList = badges.getJSONArray("badges");
        for (int i = 0; i < badgesList.length(); ++i) {
            String name = "ffz-" + badgesList.getJSONObject(i).getString("name");
            ffzBadges.put(name, new Hashtable<String, Object>());

            String imageLocation = "https:" + badgesList.getJSONObject(i).getJSONObject("urls").getString("2") + "/solid";
            ffzBadges.get(name).put("image", imageLocation);
            ffzBadges.get(name).put("users", new ArrayList<String>());

            JSONArray userList = badges.getJSONObject("users").getJSONArray(badgesList.getJSONObject(i).getString("id"));
            for (int j = 0; j < userList.length(); ++j) {
                ((ArrayList) ffzBadges.get(name).get("users")).add(userList.getString(j).toLowerCase());
            }
        }
    }

    private void getBTTVGlobalEmotes() throws Exception {
        URL globalURL = new URL(bttvAPIURL + "emotes/global");
        JSONResponse response = getJSON(globalURL);
        int status = response.getStatusCode();
        if (status != 200) {
            XposedBridge.log("LoN: Error fetching bttv global emotes (" + status + ")");
            return;
        }

        JSONArray globalEmotesArray = response.jsonAsArray();
        if (globalEmotesArray.length() == 0) {
            XposedBridge.log("LoN: BTTV global emotes came back empty");
            return;
        }

        for (int i = 0; i < globalEmotesArray.length(); ++i) {
            if(!(preferences.disableGifEmotes() && globalEmotesArray.getJSONObject(i).getString("imageType").equals("gif"))) {
                String emoteName = globalEmotesArray.getJSONObject(i).getString("code");
                String emoteID = globalEmotesArray.getJSONObject(i).getString("id");
                String emoteURL = bttvUrlTemplate.replace("{{id}}", emoteID).replace("{{image}}", "1x");
                bttvGlobalEmotes.put(emoteName, emoteURL);
            }
        }
    }

    private void getBTTVRoomEmotes(int channelId) throws Exception {
        bttvRoomEmotes.clear();
        URL roomURL = new URL(bttvAPIURL + "users/twitch/" + channelId);
        JSONObject roomEmotes = getJSON(roomURL).jsonAsObject();
        int status = roomEmotes.getInt("status");
        if (status != 200) {
            if (status != 404) {
                XposedBridge.log("LoN: Error fetching bttv room emotes (" + status + ")");
            }
            return;
        }

        JSONArray roomEmotesArray = roomEmotes.getJSONArray("channelEmotes");
        JSONArray sharedEmotesArray = roomEmotes.getJSONArray("sharedEmotes");
        for (int i = 0; i < roomEmotesArray.length(); ++i) {
            if(!(preferences.disableGifEmotes() && roomEmotesArray.getJSONObject(i).getString("imageType").equals("gif"))) {
                String emoteName = roomEmotesArray.getJSONObject(i).getString("code");
                String emoteID = roomEmotesArray.getJSONObject(i).getString("id");
                String emoteURL = bttvUrlTemplate.replace("{{id}}", emoteID).replace("{{image}}", "1x");
                bttvRoomEmotes.put(emoteName, emoteURL);
            }
        }
        for (int i = 0; i < sharedEmotesArray.length(); ++i) {
            if(!(preferences.disableGifEmotes() && sharedEmotesArray.getJSONObject(i).getString("imageType").equals("gif"))) {
                String emoteName = sharedEmotesArray.getJSONObject(i).getString("code");
                String emoteID = sharedEmotesArray.getJSONObject(i).getString("id");
                String emoteURL = bttvUrlTemplate.replace("{{id}}", emoteID).replace("{{image}}", "1x");
                bttvRoomEmotes.put(emoteName, emoteURL);
            }
        }
    }

    @SuppressWarnings({"unchecked", "ConstantConditions"})
    private void getBTTVBadges() throws Exception {
        URL badgeURL = new URL(bttvAPIURL + "badges");
        JSONResponse response = getJSON(badgeURL);
        if (response.getStatusCode() != 200) {
            XposedBridge.log("LoN: Error fetching bttv badges");
            return;
        }

        JSONArray badges = response.jsonAsArray();
        if (badges.length() == 0) {
            XposedBridge.log("LoN: BTTV badges came back empty");
            return;
        }

        Hashtable<String, String> badgeConversion = new Hashtable();
        badgeConversion.put("NightDev Developer",       "bttv-developer");
        badgeConversion.put("NightDev Support Team",    "bttv-support");
        badgeConversion.put("NightDev Design Team",     "bttv-design");
        badgeConversion.put("BetterTTV Emote Approver", "bttv-emotes");
        for (int i = 0; i < badges.length(); ++i) {
            String name = badgeConversion.get(badges.getJSONObject(i).getJSONObject("badge").getString("description"));
            String user = badges.getJSONObject(i).getString("name");
            if (bttvBadges.get(name) == null) {
                bttvBadges.put(name, new Hashtable<String, Object>());
                String imageLocation = "";
                switch(name) {
                    case "bttv-developer": { imageLocation = "https://leagueofnewbs.com/images/bttv-dev.png"; break; }
                    case "bttv-support": { imageLocation = "https://leagueofnewbs.com/images/bttv-support.png"; break; }
                    case "bttv-design": { imageLocation = "https://leagueofnewbs.com/images/bttv-design.png"; break; }
                    case "bttv-emotes": { imageLocation = "https://leagueofnewbs.com/images/bttv-approver.png"; break; }
                }
                bttvBadges.get(name).put("image", imageLocation);
                bttvBadges.get(name).put("users", new ArrayList<String>());
            }

            ((ArrayList) bttvBadges.get(name).get("users")).add(user);
        }
    }

    private JSONResponse getJSON(URL url) throws Exception {
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("User-Agent", "Glitchify|[email protected]");
        if (url.getHost().contains("twitch.tv")) {
            conn.setRequestProperty("Client-ID", "2pvhvz6iubpg0ny77pyb1qrjynupjdu");
        }
        InputStream inStream;
        int responseCode = conn.getResponseCode();
        if (responseCode >= 400) {
            inStream = conn.getErrorStream();
        } else {
            inStream = conn.getInputStream();
        }
        BufferedReader buffReader = new BufferedReader(new InputStreamReader(inStream));
        StringBuilder jsonString = new StringBuilder();
        String line;
        while ((line = buffReader.readLine()) != null) {
            jsonString.append(line);
        }
        buffReader.close();

        return new JSONResponse(responseCode, jsonString.toString());
    }

    private void printException(Exception e, String prefix) {
        if (e.getMessage() == null || e.getMessage().equals("")) {
            return;
        }
        String output = "LoN: ";
        if (prefix != null) {
            output += prefix;
        }
        output += e.getMessage();
        XposedBridge.log(output);
        Log.e(logTag, output, e);
    }

}