/**
 * rscplus
 *
 * <p>This file is part of rscplus.
 *
 * <p>rscplus is free software: you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * <p>rscplus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * <p>You should have received a copy of the GNU General Public License along with rscplus. If not,
 * see <http://www.gnu.org/licenses/>.
 *
 * <p>Authors: see <https://github.com/RSCPlus/rscplus>
 */
package Game;

import Client.Launcher;
import Client.Logger;
import Client.NotificationsHandler;
import Client.NotificationsHandler.NotifType;
import Client.Settings;
import Client.Util;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageConsumer;
import java.io.File;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageIO;

/** Handles rendering overlays and client adjustments based on window size */
public class Renderer {

  public static Object instance = null;

  public static int width;
  public static int height;
  public static int height_client;
  public static int[] pixels;

  public static int fps;
  public static float alpha_time;
  public static float delta_time;
  public static long time;

  public static ImageConsumer image_consumer = null;

  public static Color color_dynamic;
  public static Color color_text = new Color(240, 240, 240);
  public static Color color_shadow = new Color(15, 15, 15);
  public static Color color_gray = new Color(60, 60, 60);
  public static Color color_hp = new Color(0, 210, 0);
  public static Color color_fatigue = new Color(210, 210, 0);
  public static Color color_prayer = new Color(160, 160, 210);
  public static Color color_low = new Color(255, 0, 0);
  public static Color color_poison = new Color(155, 205, 50);
  public static Color color_item = new Color(245, 245, 245);
  public static Color color_item_highlighted = new Color(245, 196, 70);
  public static Color color_replay = new Color(100, 185, 178);
  public static Color color_white = new Color(255, 255, 255);
  public static Color color_yellow = new Color(255, 255, 0);

  public static Image image_border;
  public static Image image_bar_frame;
  public static Image image_cursor;
  public static Image image_highlighted_item;
  private static BufferedImage game_image;

  private static Dimension new_size = new Dimension(0, 0);

  private static Item last_item;

  private static Font font_main;
  private static Font font_big;

  private static int frames = 0;
  private static long fps_timer = 0;
  private static boolean screenshot = false;

  public static boolean combat_menu_shown = false;

  public static int replayOption = 0;

  public static String[] shellStrings;

  private static boolean macOS_resize_workaround = Util.isMacOS();

  public static boolean quietScreenshot = false;

  public static Rectangle barBounds;
  public static Rectangle previousBounds;
  public static Rectangle slowForwardBounds;
  public static Rectangle playPauseBounds;
  public static Rectangle fastForwardBounds;
  public static Rectangle nextBounds;
  public static Rectangle stopBounds;
  public static Rectangle queueBounds;
  private static int shapeHeight;
  private static int shapeX;

  public static void init() {
    // patch copyright to match the year that jagex took down RSC
    shellStrings[23] = shellStrings[23].replaceAll("2015", "2018");

    // Resize game window
    new_size.width = 512;
    new_size.height = 346;
    handle_resize();

    // Load fonts
    try {
      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      InputStream is = Settings.getResourceAsStream("/assets/Helvetica-Bold.ttf");
      Font font = Font.createFont(Font.TRUETYPE_FONT, is);
      ge.registerFont(font);
      font_main = font.deriveFont(Font.PLAIN, 11.0f);
      font_big = font.deriveFont(Font.PLAIN, 22.0f);

      is = Settings.getResourceAsStream("/assets/TimesRoman.ttf");
      ge.registerFont(Font.createFont(Font.TRUETYPE_FONT, is));
    } catch (Exception e) {
      e.printStackTrace();
    }

    // Load images
    try {
      image_border = ImageIO.read(Settings.getResource("/assets/border.png"));
      image_bar_frame = ImageIO.read(Settings.getResource("/assets/bar.png"));
      image_cursor = ImageIO.read(Settings.getResource("/assets/cursor.png"));
      image_highlighted_item = ImageIO.read(Settings.getResource("/assets/highlighted_item.png"));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void resize(int w, int h) {
    new_size.width = w;
    new_size.height = h;
  }

  public static void handle_resize() {
    width = new_size.width;
    height = new_size.height;

    height_client = height - 12;
    pixels = new int[width * height];
    game_image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

    Camera.resize();
    Menu.resize();

    if (image_consumer != null) image_consumer.setDimensions(width, height);

    if (Client.strings != null)
      Client.strings[262] =
          fixLengthString("~" + (Renderer.width - (512 - 439)) + "~@whi@Remove         WWWWWWWWWW");

    if (Renderer.instance != null && Reflection.setGameBounds != null) {
      try {
        Reflection.setGameBounds.invoke(
            Renderer.instance, 0, Renderer.width, Renderer.height, 0, (byte) 119);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  private static int lastPercentHP = 100;
  private static int lastFatigue = 0;

  private static float lastBaseDrainRate = 0;
  private static float lastAdjustedDrainRate = 0;

  public static void present(Graphics g, Image image) {
    // Update timing
    long new_time = System.currentTimeMillis();
    delta_time = (float) (new_time - time) / 1000.0f;
    time = new_time;
    alpha_time = 0.25f + (((float) Math.sin(time / 100) + 1.0f) / 2.0f * 0.75f);

    // This workaround is required to use custom resolution on macOS
    if (macOS_resize_workaround) {
      if (Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) {
        Game.getInstance().resizeFrameWithContents();
      } else {
        Game.getInstance().pack();
        Game.getInstance().setLocationRelativeTo(null);
      }
      macOS_resize_workaround = false;
    }

    // Reset dialogue option after force pressed in replay
    if (Replay.isPlaying && KeyboardHandler.dialogue_option != -1)
      KeyboardHandler.dialogue_option = -1;

    Graphics2D g2 =
        (Graphics2D) game_image.getGraphics(); // TODO: Declare g2 outside of the present method
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setFont(font_main);

    g2.drawImage(image, 0, 0, null);
    g2.drawImage(image_border, 512, height - 13, width - 512, 13, null);

    // In-game UI
    if (Client.state == Client.STATE_GAME) {
      int npcCount = 0;
      int playerCount = 0;

      // Update player coords
      for (Iterator<NPC> iterator = Client.npc_list.iterator(); iterator.hasNext(); ) {
        NPC npc = iterator.next(); // TODO: Remove unnecessary allocations
        if (npc != null) {
          if (npc.type == NPC.TYPE_PLAYER) playerCount++;
          else if (npc.type == NPC.TYPE_MOB) npcCount++;

          if (Client.player_name.equals(npc.name)) {
            Client.player_posX = npc.x;
            Client.player_posY = npc.y;
            Client.player_height = npc.height;
            Client.player_width = npc.width;

            Client.isGameLoaded = true;
          }
        }
      }

      if (!Client.isInterfaceOpen() && Client.show_menu == Client.MENU_NONE) {
        List<Rectangle> npc_hitbox = new ArrayList<>();
        List<Rectangle> player_hitbox = new ArrayList<>();
        List<Point> entity_text_loc = new ArrayList<>();

        for (Iterator<NPC> iterator = Client.npc_list.iterator(); iterator.hasNext(); ) {
          NPC npc = iterator.next(); // TODO: Remove unnecessary allocations
          Color color = color_low;

          boolean show = false;
          boolean extend = false;
          if (npc.type == NPC.TYPE_PLAYER) {
            color = color_fatigue;

            if (Client.isFriend(npc.name)
                && (Settings.SHOW_FRIEND_NAME_OVERLAY.get(Settings.currentProfile)
                    || Settings.SHOW_PLAYER_NAME_OVERLAY.get(Settings.currentProfile))) {
              color = color_hp;
              show = true;
            } else if (Settings.SHOW_PLAYER_NAME_OVERLAY.get(Settings.currentProfile)) {
              show = true;
            }
          } else if (npc.type == NPC.TYPE_MOB
              && Settings.SHOW_NPC_NAME_OVERLAY.get(Settings.currentProfile)) {
            show = true;
          }

          if (Settings.SHOW_HITBOX.get(Settings.currentProfile)) {
            List<Rectangle> hitbox = player_hitbox;
            boolean showHitbox = true;

            if (npc.type == NPC.TYPE_MOB) hitbox = npc_hitbox;

            for (Iterator<Rectangle> boxIterator = hitbox.iterator(); boxIterator.hasNext(); ) {
              Rectangle rect = boxIterator.next(); // TODO: Remove unnecessary allocations
              if (rect.x == npc.x
                  && rect.y == npc.y
                  && rect.width == npc.width
                  && rect.height == npc.height) {
                showHitbox = false;
                break;
              }
            }

            if (showHitbox) {
              setAlpha(g2, 0.3f);
              g2.setColor(color);
              g2.fillRect(npc.x, npc.y, npc.width, npc.height);
              g2.setColor(Color.BLACK);
              g2.drawRect(npc.x, npc.y, npc.width, npc.height);
              setAlpha(g2, 1.0f);
              hitbox.add(new Rectangle(npc.x, npc.y, npc.width, npc.height));
            }
          }

          if (Settings.SHOW_HP_PRAYER_FATIGUE_OVERLAY.get(Settings.currentProfile)
              && npc.name != null) {
            int x = npc.x + (npc.width / 2);
            int y = npc.y - 20;
            for (Iterator<Point> locIterator = entity_text_loc.iterator();
                locIterator.hasNext(); ) {
              Point loc = locIterator.next(); // TODO: Remove unnecessary allocations
              if (loc.x == x && loc.y == y) y -= 12;
            }
            if (show) {
              extend = Settings.EXTEND_IDS_OVERLAY.get(Settings.currentProfile);
              String text = npc.name;
              if (extend) {
                text += (" (" + npc.id + "-" + npc.id2 + ")");
              }
              drawShadowText(g2, text, x, y, color, true);
            }
            entity_text_loc.add(new Point(x, y));
          }
        }

        List<Rectangle> item_hitbox = new ArrayList<>();
        List<Point> item_text_loc = new ArrayList<>();

        if (Settings.SHOW_ITEM_GROUND_OVERLAY.get(
            Settings.currentProfile)) { // Don't sort if we aren't displaying any item names anyway
          try {
            // Keep items in (technically reverse) alphabetical order for SHOW_ITEMINFO instead of
            // randomly
            // changing places each frame
            Collections.sort(Client.item_list, new ItemComparator());
          } catch (Exception e) {
            // Sometimes Java helpfully complains that the sorting method violates its general
            // contract.
            e.printStackTrace();
          }
        }

        for (Iterator<Item> iterator = Client.item_list.iterator(); iterator.hasNext(); ) {
          Item item = iterator.next(); // TODO: Remove unnecessary allocations

          if (Settings.SHOW_HITBOX.get(Settings.currentProfile)) {
            boolean show = true;
            for (Iterator<Rectangle> boxIterator = item_hitbox.iterator();
                boxIterator.hasNext(); ) {
              Rectangle rect = boxIterator.next(); // TODO: Remove unnecessary allocations
              if (rect.x == item.x
                  && rect.y == item.y
                  && rect.width == item.width
                  && rect.height == item.height) {
                show = false;
                break;
              }
            }

            if (show) {
              setAlpha(g2, 0.3f);
              g2.setColor(color_prayer);
              g2.fillRect(item.x, item.y, item.width, item.height);
              g2.setColor(Color.BLACK);
              g2.drawRect(item.x, item.y, item.width, item.height);
              setAlpha(g2, 1.0f);
              item_hitbox.add(new Rectangle(item.x, item.y, item.width, item.height));
            }
          }

          if (Settings.SHOW_ITEM_GROUND_OVERLAY.get(Settings.currentProfile)) {
            int x = item.x + (item.width / 2);
            int y = item.y - 20;
            int freq = Collections.frequency(Client.item_list, item);

            // Check if item is in blocked list
            boolean itemIsBlocked =
                stringIsWithinList(item.getName(), Settings.BLOCKED_ITEMS.get("custom"));

            // We've sorted item list in such a way that it is possible to not draw the ITEMINFO
            // unless it's the first time we've tried to for this itemid at that location
            // by just using last_item.
            // last_item == null necessary in case only one item on screen is being rendered. slight
            // speed increase from freq == 1 if compiler can stop early in conditional.
            if ((freq == 1 || !item.equals(last_item) || last_item == null) && !itemIsBlocked) {
              for (Iterator<Point> locIterator = item_text_loc.iterator();
                  locIterator.hasNext(); ) {
                Point loc = locIterator.next(); // TODO: Remove unnecessary allocations
                if (loc.x == x && loc.y == y) {
                  y -= 12;
                }
              }
              item_text_loc.add(new Point(x, y));

              Color itemColor = color_item;
              String itemText = item.getName() + ((freq == 1) ? "" : " (" + freq + ")");

              // Check if item is in highlighted list
              if (stringIsWithinList(item.getName(), Settings.HIGHLIGHTED_ITEMS.get("custom"))) {
                itemColor = color_item_highlighted;
                drawHighlighImage(g2, itemText, x, y);
              }

              // TODO: it would be nice if for items like Coins or Runes, we showed how many of the
              // item were on the ground instead of how many times you have to click to pick them
              // all up.
              // Currently will just show "Coins (2)" if there are two stacks of coins on the
              // ground.
              drawShadowText(g2, itemText, x, y, itemColor, true);
            }
            last_item = item; // Done with item this loop, can save it as last_item
          }
        }

        Client.processFatigueXPDrops();
      }

      if (!Client.isSleeping()) {
        Client.updateCurrentFatigue();
      }

      // Clear item list for next frame
      Client.item_list.clear();
      last_item = null;

      if (!Client.show_sleeping && Settings.SHOW_INVCOUNT.get(Settings.currentProfile))
        drawShadowText(
            g2,
            Client.inventory_count + "/" + Client.max_inventory,
            width - 19,
            17,
            color_text,
            true);

      int percentHP = 0;
      int percentPrayer = 0;
      float alphaHP = 1.0f;
      float alphaPrayer = 1.0f;
      float alphaFatigue = 1.0f;
      Color colorHP = color_hp;
      Color colorPrayer = color_prayer;
      Color colorFatigue = color_fatigue;

      if (Client.getBaseLevel(Client.SKILL_HP) > 0) {
        percentHP =
            Client.getCurrentLevel(Client.SKILL_HP) * 100 / Client.getBaseLevel(Client.SKILL_HP);
        percentPrayer =
            Client.getCurrentLevel(Client.SKILL_PRAYER)
                * 100
                / Client.getBaseLevel(Client.SKILL_PRAYER);
      }

      if (percentHP < 30) {
        colorHP = color_low;
        alphaHP = alpha_time;
      }

      if (percentPrayer < 30) {
        colorPrayer = color_low;
        alphaPrayer = alpha_time;
      }

      if (Client.getFatigue() >= 80) {
        colorFatigue = color_low;
        alphaFatigue = alpha_time;
      }

      // Low HP notification
      if (percentHP <= Settings.LOW_HP_NOTIF_VALUE.get(Settings.currentProfile)
          && lastPercentHP > percentHP
          && lastPercentHP > Settings.LOW_HP_NOTIF_VALUE.get(Settings.currentProfile))
        NotificationsHandler.notify(
            NotifType.LOWHP, "Low HP Notification", "Your HP is at " + percentHP + "%");
      lastPercentHP = percentHP;

      // High fatigue notification
      if (Client.getFatigue() >= Settings.FATIGUE_NOTIF_VALUE.get(Settings.currentProfile)
          && lastFatigue < Client.getFatigue()
          && lastFatigue < Settings.FATIGUE_NOTIF_VALUE.get(Settings.currentProfile))
        NotificationsHandler.notify(
            NotifType.FATIGUE,
            "High Fatigue Notification",
            "Your fatigue is at " + Client.getFatigue() + "%");
      lastFatigue = Client.getFatigue();

      // Draw HP, Prayer, Fatigue overlay
      int x = 24;
      int y = 28;

      // combat menu is showing, so move everything down
      if (combat_menu_shown) y = 132;

      // NPC Post-processing for ui
      if (Settings.SHOW_COMBAT_INFO.get(Settings.currentProfile) && !Client.isInterfaceOpen()) {
        int bar_count = 0;
        for (Iterator<NPC> iterator = Client.npc_list.iterator(); iterator.hasNext(); ) {
          NPC npc = iterator.next();
          if (npc != null && Client.isInCombatWithNPC(npc)) {
            drawNPCBar(g2, 7, y, npc);
            // Increment y by npc bar height, so we can have multiple bars
            // NOTE: We should never (?) have more than one npc health bar, so multiple bars
            // indicates that our combat detection isn't accurate
            y += 50;
            bar_count++;
          }
        }
        if (bar_count > 0) {
          y += 16;
        }
      }

      if (Settings.SHOW_HP_PRAYER_FATIGUE_OVERLAY.get(Settings.currentProfile)) {
        if (width < 800) {
          if (!Client.isInterfaceOpen() && !Client.show_questionmenu) {
            setAlpha(g2, alphaHP);
            drawShadowText(
                g2,
                "Hits: "
                    + Client.current_level[Client.SKILL_HP]
                    + "/"
                    + Client.base_level[Client.SKILL_HP],
                x,
                y,
                colorHP,
                false);
            y += 16;
            setAlpha(g2, alphaPrayer);
            drawShadowText(
                g2,
                "Prayer: "
                    + Client.current_level[Client.SKILL_PRAYER]
                    + "/"
                    + Client.base_level[Client.SKILL_PRAYER],
                x,
                y,
                colorPrayer,
                false);
            y += 16;
            setAlpha(g2, alphaFatigue);
            drawShadowText(
                g2, "Fatigue: " + Client.getFatigue() + "/100", x, y, colorFatigue, false);
            setAlpha(g2, 1.0f);
            y += 16;
          }
        } else {
          int barSize = 4 + image_bar_frame.getWidth(null);
          int x2 = width - (4 + barSize);
          int y2 = height - image_bar_frame.getHeight(null);

          drawBar(
              g2, image_bar_frame, x2, y2, colorFatigue, alphaFatigue, Client.getFatigue(), 100);
          x2 -= barSize;

          drawBar(
              g2,
              image_bar_frame,
              x2,
              y2,
              colorPrayer,
              alphaPrayer,
              Client.current_level[Client.SKILL_PRAYER],
              Client.base_level[Client.SKILL_PRAYER]);
          x2 -= barSize;

          drawBar(
              g2,
              image_bar_frame,
              x2,
              y2,
              colorHP,
              alphaHP,
              Client.current_level[Client.SKILL_HP],
              Client.base_level[Client.SKILL_HP]);
          x2 -= barSize;
        }
      }

      // Draw under combat style info
      // buffs, debuffs and cooldowns
      if (!Client.isInterfaceOpen() && Settings.SHOW_BUFFS.get(Settings.currentProfile)) {
        if (time <= Client.magic_timer) {
          float timer = (float) Math.ceil((Client.magic_timer - time) / 1000.0);
          drawShadowText(g2, "Magic Timer: " + (int) timer, x, y, color_text, false);
          y += 14;
        }

        for (int i = 0; i < 18; i++) {
          if (Client.current_level[i] != Client.base_level[i]
              && (i != Client.SKILL_HP && i != Client.SKILL_PRAYER)) {
            int diff = Client.current_level[i] - Client.base_level[i];
            Color color = color_low;

            // Build our boost string
            // If the difference is greater than 0 (positive boost), we need to add a "+" to the
            // string
            // Otherwise, it can be left alone because the integer will already have a "-" when
            // converted to a string
            String boost = Integer.toString(diff);
            if (diff > 0) {
              boost = "+" + boost;
              color = color_hp;
            }

            drawShadowText(g2, boost, x, y, color, false);
            drawShadowText(g2, Client.skill_name[i], x + 32, y, color, false);
            y += 14;
          }
        }

        int base_drain_rate = 0;
        float adjusted_drain_rate = 0;
        // 14 selectable prayers
        for (int i = 0; i < 14; i++) {
          if (Client.prayers_on[i] == true) base_drain_rate += Client.DRAIN_RATES[i];
        }
        lastBaseDrainRate = base_drain_rate;
        if (base_drain_rate != 0) {
          // with prayer equipment, combat rounds get increased about 3.1% per +1
          float factor = 1.0f;
          if (Client.current_equipment_stats[4] > 1) {
            int boost = Client.current_equipment_stats[4] - 1;
            float increase_rounds = boost * 0.031f;
            // percentage of increase per round
            factor = 1 + increase_rounds;
          }
          adjusted_drain_rate = base_drain_rate / factor;
          lastAdjustedDrainRate = adjusted_drain_rate;
          // a drain_rate of 60 drains 1 point in 3.33 secs
          // 75 drains 1.25 points in 3.33 secs -> 3.33/(adjusted/60)
          float points_psec = (3.33f * 60.0f) / adjusted_drain_rate;

          drawShadowText(g2, "-1", x, y, color_low, false);
          drawShadowText(
              g2, "Prayer/" + Client.trimNumber(points_psec, 1) + "s", x + 32, y, color_low, false);
          y += 14;
        } else {
          // no prayer armour adjusting drain rate
          lastAdjustedDrainRate = lastBaseDrainRate;
        }
        if (time > Client.poison_timer && Client.is_poisoned) {
          // more than 20 seconds passed and last status was poison, user probably is no longer
          // poisoned
          Client.is_poisoned = false;
          Client.poison_timer = time;
        }
        if (Client.is_poisoned) {
          drawShadowText(g2, "Poisoned!", x, y, color_poison, false);
          y += 14;
        }
      }

      // Clear npc list for the next frame
      Client.npc_list.clear();

      Client.xpdrop_handler.draw(g2);
      Client.xpbar.draw(g2);

      if (Settings.DEBUG.get(Settings.currentProfile)) {
        x = 32;
        y = 32;

        // Draw Skills
        for (int i = 0; i < 18; i++) {
          drawShadowText(
              g2,
              Client.skill_name[i]
                  + " ("
                  + i
                  + "): "
                  + Client.current_level[i]
                  + "/"
                  + Client.base_level[i]
                  + " ("
                  + Client.getXP(i)
                  + " xp)",
              x,
              y,
              color_text,
              false);
          y += 16;
        }

        // Draw Fatigue
        y += 16;
        drawShadowText(
            g2, "Fatigue: " + ((float) Client.fatigue * 100.0f / 750.0f), x, y, color_text, false);
        y += 16;

        // Draw Drain rates
        y += 16;
        drawShadowText(g2, "Base Drain Rate: " + lastBaseDrainRate, x, y, color_text, false);
        y += 16;
        drawShadowText(
            g2,
            "Adjusted Drain Rate: " + Client.trimNumber(lastAdjustedDrainRate, 1),
            x,
            y,
            color_text,
            false);
        y += 16;

        // Draw Mouse Info
        y += 16;
        drawShadowText(
            g2,
            "Mouse Position: " + MouseHandler.x + ", " + MouseHandler.y,
            x,
            y,
            color_text,
            false);
        y += 16;

        // Draw camera info
        y += 16;
        drawShadowText(g2, "Camera Rotation: " + Camera.rotation, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Angle: " + Camera.angle, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Auto: " + Camera.auto, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Auto Speed: " + Camera.auto_speed, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Rotation Y: " + Camera.rotation_y, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Lookat X: " + Camera.lookat_x, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Lookat Y: " + Camera.lookat_y, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Zoom: " + Camera.zoom, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Distance1: " + Camera.distance1, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Distance2: " + Camera.distance2, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Distance3: " + Camera.distance3, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Camera Distance4: " + Camera.distance4, x, y, color_text, false);
        y += 16;

        x = 256;
        y = 32;
        drawShadowText(
            g2, "FPS: " + fps + " (" + Client.updatesPerSecond + ")", x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Game Size: " + width + "x" + height, x, y, color_text, false);
        y += 16;

        // Draw Inventory items
        y += 16;
        for (int i = 0; i < Client.inventory_count; i++) {
          drawShadowText(g2, "(" + i + "): " + Client.inventory_items[i], x, y, color_text, false);
          y += 16;
        }

        y += 16;
        drawShadowText(g2, "Menu: " + Client.show_menu, x, y, color_text, false);
        y += 16;

        x = 380;
        y = 32;
        drawShadowText(g2, Client.player_name, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Player Count: " + playerCount, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "NPC Count: " + npcCount, x, y, color_text, false);
        y += 16;
        drawShadowText(
            g2,
            "LocalRegion: (" + Client.localRegionX + "," + Client.localRegionY + ")",
            x,
            y,
            color_text,
            false);
        y += 16;
        drawShadowText(
            g2, "Region: (" + Client.regionX + "," + Client.regionY + ")", x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "WorldCoord: " + Client.getCoords(), x, y, color_text, false);
        y += 16;
        drawShadowText(
            g2,
            "Plane: ("
                + Client.planeWidth
                + ","
                + Client.planeHeight
                + ","
                + Client.planeIndex
                + ")",
            x,
            y,
            color_text,
            false);
        y += 16;
        drawShadowText(g2, "combat_timer: " + Client.combat_timer, x, y, color_text, false);
        y += 32;
        drawShadowText(g2, "frame_time_slice: " + Replay.frame_time_slice, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "lag: " + Replay.timestamp_lag + " updates", x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "replay_timestamp: " + Replay.timestamp, x, y, color_text, false);
        y += 16;
        drawShadowText(
            g2,
            "replay_server_timestamp: " + Replay.timestamp_server_last,
            x,
            y,
            color_text,
            false);
        y += 16;
        drawShadowText(
            g2, "replay_client_timestamp: " + Replay.timestamp_client, x, y, color_text, false);
        y += 16;
        drawShadowText(
            g2, "replay_client_read: " + Replay.getClientRead(), x, y, color_text, false);
        y += 16;
        drawShadowText(
            g2, "replay_client_write: " + Replay.getClientWrite(), x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Last sound effect: " + Client.lastSoundEffect, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Mouse Text: " + Client.mouseText, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Hover: " + Client.is_hover, x, y, color_text, false);
        y += 16;
        drawShadowText(g2, "Java version: " + Settings.javaVersion, x, y, color_text, false);
      }

      // A little over a full tick
      int threshold = 35;

      if (Replay.isPlaying && Replay.fpsPlayMultiplier > 1.0)
        threshold = 35 * 3; // this is to prevent blinking during fastforward

      if (Settings.LAG_INDICATOR.get(Settings.currentProfile)
          && Replay.getServerLag() >= threshold) {
        x = width - 80;
        y = height - 80;
        setAlpha(g2, alpha_time);
        g2.drawImage(Launcher.icon_warn.getImage(), x, y, 32, 32, null);
        x += 16;
        y += 38;
        drawShadowText(g2, "Server Lag", x, y, color_fatigue, true);
        y += 12;
        int lag = (Replay.getServerLag() - 31) * Replay.getFrameTimeSlice();
        drawShadowText(
            g2,
            new DecimalFormat("0.0").format((float) lag / 1000.0f) + "s",
            x,
            y,
            color_low,
            true);
        setAlpha(g2, 1.0f);
      }
      if (!(Replay.isPlaying && !Settings.TRIGGER_ALERTS_REPLAY.get(Settings.currentProfile))) {
        g2.setFont(font_big);
        if (Settings.FATIGUE_ALERT.get(Settings.currentProfile)
            && Client.getFatigue() >= 98
            && !Client.isInterfaceOpen()) {
          setAlpha(g2, alpha_time);
          drawShadowText(g2, "FATIGUED", width / 2, height / 2, color_low, true);
          setAlpha(g2, 1.0f);
        }
        if (Settings.INVENTORY_FULL_ALERT.get(Settings.currentProfile)
            && Client.inventory_count >= 30
            && !Client.isInterfaceOpen()) {
          setAlpha(g2, alpha_time);
          drawShadowText(g2, "INVENTORY FULL", width / 2, height / 2, color_low, true);
          setAlpha(g2, 1.0f);
        }
        g2.setFont(font_main);
      }

      if (Settings.SHOW_PLAYER_POSITION.get(Settings.currentProfile)) {
        y = Renderer.height - 19;
        int offset = 0;
        if (Client.is_in_wild) offset += 70;
        if ((!screenshot && Replay.isPlaying && Settings.SHOW_SEEK_BAR.get(Settings.currentProfile))
            || Settings.SHOW_RETRO_FPS.get(Settings.currentProfile)) y -= 12;
        if ((!Replay.isPlaying || screenshot)
            && Settings.SHOW_RETRO_FPS.get(Settings.currentProfile)) offset += 70;
        drawShadowText(
            g2,
            "Pos: " + Client.getCoords(),
            (Renderer.width - 92 - offset),
            y,
            color_yellow,
            false);
      }

      // Mouseover hover handling
      if (Settings.SHOW_MOUSE_TOOLTIP.get(Settings.currentProfile)
          && !Client.isInterfaceOpen()
          && !Client.show_questionmenu
          && Client.is_hover) {
        String cleanText = Client.mouseText;
        String extraOptions = "";
        final int extraOptionsOffsetX = 8;
        final int extraOptionsOffsetY = 12;
        int indexExtraOptions = cleanText.indexOf('/');

        String colorlessText = cleanText;

        // Remove extra options text
        if (indexExtraOptions != -1) cleanText = cleanText.substring(0, indexExtraOptions).trim();

        // Remove color codes from string
        for (int i = 0; i < colorlessText.length(); i++) {
          if (colorlessText.charAt(i) == '@') {
            try {
              if (colorlessText.charAt(i + 4) == '@')
                colorlessText = colorlessText.substring(0, i) + colorlessText.substring(i + 5);
            } catch (Exception e) {
            }
          }
        }

        // Let's grab the extra options
        indexExtraOptions = colorlessText.indexOf('/');
        if (indexExtraOptions != -1) {
          extraOptions = colorlessText.substring(indexExtraOptions + 1).trim();
          colorlessText = colorlessText.substring(0, indexExtraOptions).trim();
        }

        if (extraOptions.length() > 0) extraOptions = "(" + extraOptions + ")";

        x = MouseHandler.x + 16;
        y = MouseHandler.y + 28;

        // Dont allow text to go off the screen
        Dimension bounds = getStringBounds(g2, colorlessText);
        Dimension extraBounds = getStringBounds(g2, extraOptions);
        if (extraOptions.length() == 0) extraBounds.height = 0;

        bounds.height += extraOptionsOffsetY;
        if (Settings.SHOW_EXTENDED_TOOLTIP.get(Settings.currentProfile)) {
          extraBounds.width += extraOptionsOffsetX;
          bounds.width = (bounds.width > extraBounds.width) ? bounds.width : extraBounds.width;
          bounds.height += extraBounds.height;
        }
        if (x + bounds.width > Renderer.width - 4) x -= (x + bounds.width) - (Renderer.width - 4);
        if (y + bounds.height > Renderer.height) y -= (y + bounds.height) - (Renderer.height);

        indexExtraOptions = cleanText.indexOf(":");
        if (indexExtraOptions != -1) {
          String name = cleanText.substring(0, indexExtraOptions).trim();
          String action = cleanText.substring(indexExtraOptions + 1).trim();
          cleanText = action + " " + name;
        }

        // Draw the final outcome
        if (Settings.SHOW_EXTENDED_TOOLTIP.get(Settings.currentProfile)) {
          setAlpha(g2, 0.65f);
          g2.setColor(color_shadow);
          g2.fillRect(x - 4, y - 12, bounds.width + 8, bounds.height - 8);
          setAlpha(g2, 1.0f);
          drawColoredText(g2, cleanText, x, y);
          x += extraOptionsOffsetX;
          y += extraOptionsOffsetY;
          drawColoredText(g2, "@whi@" + extraOptions, x, y);
        } else {
          if (!cleanText.contains("Walk here") && !cleanText.contains("Choose a target")) {
            setAlpha(g2, 0.65f);
            g2.setColor(color_shadow);
            g2.fillRect(x - 4, y - 12, bounds.width + 8, bounds.height - 8);
            setAlpha(g2, 1.0f);
            drawColoredText(g2, cleanText, x, y);
          }
        }
      }
    } else if (Client.state == Client.STATE_LOGIN) {
      if (Settings.DEBUG.get(Settings.currentProfile))
        drawShadowText(g2, "DEBUG MODE", 38, 8, color_text, true);

      // Draw world list
      drawShadowText(g2, "World (Click to change): ", 80, height - 8, color_text, true);
      for (int i = 1; i <= Settings.WORLDS_TO_DISPLAY; i++) {
        Rectangle bounds = new Rectangle(134 + (i * 18), height - 12, 16, 12);
        Color color = color_text;

        if (i == Settings.WORLD.get(Settings.currentProfile)) color = color_low;

        setAlpha(g2, 0.5f);
        g2.setColor(color);
        g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
        setAlpha(g2, 1.0f);
        String worldString = Integer.toString(i);
        drawShadowText(
            g2, worldString, bounds.x + (bounds.width / 2), bounds.y + 4, color_text, true);

        // Handle world selection click
        if (MouseHandler.x >= bounds.x
            && MouseHandler.x <= bounds.x + bounds.width
            && MouseHandler.y >= bounds.y
            && MouseHandler.y <= bounds.y + bounds.height
            && MouseHandler.mouseClicked) {
          Game.getInstance().getJConfig().changeWorld(i);
        }
      }

      // TODO: This will need to be adjusted when the login screen is resizable
      Rectangle bounds = new Rectangle(512 - 148, 346 - 36, 48, 16);
      drawShadowText(g2, "-server replay-", bounds.x + 48, bounds.y - 10, color_fatigue, true);

      setAlpha(g2, 0.5f);
      if (replayOption == 1 || Settings.RECORD_AUTOMATICALLY.get(Settings.currentProfile)) {
        g2.setColor(color_low);
      } else {
        g2.setColor(color_text);
      }
      g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);

      if (Settings.RECORD_AUTOMATICALLY.get(Settings.currentProfile)) {
        g2.setColor(color_text);
        g2.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
      }

      setAlpha(g2, 1.0f);
      drawShadowText(g2, "record", bounds.x + (bounds.width / 2), bounds.y + 6, color_text, true);
      // Handle replay record selection click
      if (MouseHandler.x >= bounds.x
          && MouseHandler.x <= bounds.x + bounds.width
          && MouseHandler.y >= bounds.y
          && MouseHandler.y <= bounds.y + bounds.height
          && MouseHandler.mouseClicked) {
        Client.showRecordAlwaysDialogue = true;

        if (replayOption == 1) {
          replayOption = 0;
        } else {
          replayOption = 1;
        }
      }
      bounds = new Rectangle(bounds.x + bounds.width + 4, bounds.y, 48, bounds.height);
      setAlpha(g2, 0.5f);
      if (replayOption == 2) g2.setColor(color_low);
      else g2.setColor(color_text);
      g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
      setAlpha(g2, 1.0f);
      drawShadowText(g2, "play", bounds.x + (bounds.width / 2), bounds.y + 6, color_text, true);
      // Handle replay play selection click
      if (MouseHandler.x >= bounds.x
          && MouseHandler.x <= bounds.x + bounds.width
          && MouseHandler.y >= bounds.y
          && MouseHandler.y <= bounds.y + bounds.height
          && MouseHandler.mouseClicked) {
        if (replayOption == 2) {
          replayOption = 0;
        } else {
          if (ReplayQueue.replayFileSelectAdd()) {
            Renderer.replayOption = 2;
            ReplayQueue.nextReplay();
          } else {
            Renderer.replayOption = 0;
          }
        }
      }

      // TODO: Uncomment this information when we can provide it again
      /*drawShadowText(g2, "Populations", width - 67, 14, color_text, false);
      int worldPopArray[];
      int totalPop = 0;
      worldPopArray = Util.getPop();
      for (int i = 1; i < worldPopArray.length; i++) {
        drawShadowText(
            g2, "W" + i + " - " + worldPopArray[i], width - 56, 14 + (15 * i), color_text, false);
        totalPop += worldPopArray[i];
      }

      drawShadowText(
          g2,
          "There are currently " + totalPop + " players online.",
          width / 2,
          8,
          color_text,
          true);
      String daysString = "RuneScape Classic has been taken offline";
      drawShadowText(g2, daysString, width / 2, 24, Renderer.color_fatigue, true);*/

      // Draw version information
      drawShadowText(
          g2,
          "rscplus v" + String.format("%8.6f", Settings.VERSION_NUMBER),
          width - 164,
          height - 2,
          color_text,
          false);
    }

    if (Client.state == Client.STATE_GAME && Replay.isPlaying && !screenshot) {
      if (Settings.SHOW_SEEK_BAR.get(Settings.currentProfile)) {
        float percent = (float) Replay.timestamp / Replay.getReplayEnd();

        if (Replay.isSeeking) {
          percent = (float) Replay.getSeekEnd() / Replay.getReplayEnd();
        }

        // "extended" is "in hover mode"
        boolean extended = (MouseHandler.y >= height - 28);

        // draw shadow box while hovering
        if (extended) {
          Rectangle bounds = new Rectangle(0, height - 28, width, 48);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.75f);
          g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
          setAlpha(g2, 1.0f);
          g2.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
        }

        // Set small font
        g2.setFont(font_main);

        // Handle bar
        barBounds = new Rectangle(32, height - 27, width - 64, 8);
        g2.setColor(color_text);
        setAlpha(g2, 0.25f);
        g2.fillRect(barBounds.x, barBounds.y, barBounds.width, barBounds.height);
        g2.setColor(color_prayer);
        setAlpha(g2, 0.5f);
        g2.fillRect(
            barBounds.x, barBounds.y, (int) ((float) barBounds.width * percent), barBounds.height);
        g2.setColor(color_text);
        setAlpha(g2, 1.0f);
        g2.drawRect(
            barBounds.x, barBounds.y, (int) ((float) barBounds.width * percent), barBounds.height);
        g2.setColor(color_text);
        g2.drawRect(barBounds.x, barBounds.y, barBounds.width, barBounds.height);

        // flash the bar while paused
        if (Replay.paused) {
          g2.setColor(color_low);
          setAlpha(g2, alpha_time / 5.0f);
          g2.fillRect(barBounds.x, barBounds.y, barBounds.width, barBounds.height);
          setAlpha(g2, 1.0f);
        }

        // draw time-into-replay
        String elapsed =
            Util.formatTimeDuration(Replay.elapsedTimeMillis(), Replay.endTimeMillis());
        String end = Util.formatTimeDuration(Replay.endTimeMillis(), Replay.endTimeMillis());
        if (extended) {
          drawShadowText(
              g2,
              elapsed + " / " + end,
              barBounds.x + (barBounds.width / 2),
              barBounds.y + barBounds.height + 8,
              color_replay,
              true);
        }

        float percentClient = (float) Replay.getClientRead() / Replay.getClientWrite();
        int server_x = (int) (barBounds.width * percent);
        int client_x = (int) (server_x * percentClient);

        g2.setColor(color_prayer);
        setAlpha(g2, 0.5f);
        g2.fillRect(barBounds.x + 1, barBounds.y + 1, client_x - 1, barBounds.height - 1);

        if (MouseHandler.x >= barBounds.x
            && MouseHandler.x <= barBounds.x + barBounds.width
            && MouseHandler.y >= barBounds.y
            && MouseHandler.y <= barBounds.y + barBounds.height) {
          float percentEnd = (float) (MouseHandler.x - barBounds.x) / barBounds.width;
          int timestamp = (int) (Replay.getReplayEnd() * percentEnd);
          g2.setColor(color_fatigue);
          setAlpha(g2, 0.5f);
          g2.drawLine(MouseHandler.x, barBounds.y, MouseHandler.x, barBounds.y + barBounds.height);
          setAlpha(g2, 1.0f);
          drawShadowTextBorder(
              g2,
              Util.formatTimeDuration(timestamp * 20, Replay.endTimeMillis()),
              MouseHandler.x,
              barBounds.y - 8,
              color_text,
              1.0f,
              0.75f,
              true,
              0);

          if (!Replay.isSeeking && MouseHandler.mouseClicked) Replay.seek(timestamp);
        }

        if (Replay.isSeeking) {
          drawShadowTextBorder(
              g2,
              "Seeking... Please wait",
              barBounds.x + (barBounds.width / 2),
              barBounds.y + barBounds.height - 18,
              color_fatigue,
              1.0f,
              0.75f,
              false,
              2);
        }
        // draw & handle gui "video player" control buttons
        if (extended && Settings.SHOW_PLAYER_CONTROLS.get(Settings.currentProfile)) {
          final int BUTTON_WIDTH = 30;
          final int BUTTON_HEIGHT = 11;
          final int BUTTON_OFFSET_X = 4; // how many pixels between each button horizontally
          final int BUTTON_OFFSET_Y = 4; // how many pixels "down" from the bottom of
          // the seek bar to  draw the control buttons

          // buttons are in this order:
          // previous slowforward playpause fastforward next stop --- queue

          // previous button
          previousBounds =
              new Rectangle(
                  barBounds.x,
                  barBounds.y + barBounds.height + BUTTON_OFFSET_Y,
                  BUTTON_WIDTH,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(
              previousBounds.x, previousBounds.y, previousBounds.width, previousBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              previousBounds.x + 1,
              previousBounds.y + 1,
              previousBounds.width - 1,
              previousBounds.height - 1);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          shapeHeight = previousBounds.height - 3;
          shapeX =
              previousBounds.x
                  + (int) (((float) previousBounds.width) / 2.0)
                  - (int) ((float) shapeHeight / 2.0);
          drawPlayerControlShape(g2, shapeX, previousBounds.y + 2, shapeHeight, "previous");

          if (MouseHandler.inBounds(previousBounds) && MouseHandler.mouseClicked) {
            ReplayQueue.skipped = true;
            ReplayQueue.previousReplay();
          }

          // slowdown button
          slowForwardBounds =
              new Rectangle(
                  previousBounds.x + previousBounds.width + BUTTON_OFFSET_X,
                  previousBounds.y,
                  BUTTON_WIDTH,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(
              slowForwardBounds.x,
              slowForwardBounds.y,
              slowForwardBounds.width,
              slowForwardBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              slowForwardBounds.x + 1,
              slowForwardBounds.y + 1,
              slowForwardBounds.width - 1,
              slowForwardBounds.height - 1);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          shapeHeight = slowForwardBounds.height - 3;
          shapeX =
              slowForwardBounds.x
                  + (int) (((float) slowForwardBounds.width) / 2.0)
                  - (int) ((float) shapeHeight / 2.0);
          drawPlayerControlShape(g2, shapeX, slowForwardBounds.y + 2, shapeHeight, "slowforward");

          if (MouseHandler.inBounds(slowForwardBounds) && MouseHandler.mouseClicked) {
            Replay.controlPlayback("ff_minus");
          }

          // play/pause (one button)
          playPauseBounds =
              new Rectangle(
                  slowForwardBounds.x + slowForwardBounds.width + BUTTON_OFFSET_X,
                  previousBounds.y,
                  BUTTON_WIDTH,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(
              playPauseBounds.x, playPauseBounds.y, playPauseBounds.width, playPauseBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              playPauseBounds.x + 1,
              playPauseBounds.y + 1,
              playPauseBounds.width - 1,
              playPauseBounds.height - 1);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          shapeHeight = playPauseBounds.height - 3;
          shapeX =
              playPauseBounds.x
                  + (int) (((float) playPauseBounds.width) / 2.0)
                  - (int) ((float) shapeHeight / 2.0);
          drawPlayerControlShape(g2, shapeX, playPauseBounds.y + 2, shapeHeight, "playpause");

          if (MouseHandler.inBounds(playPauseBounds) && MouseHandler.mouseClicked) {
            Replay.togglePause();
          }
          // fastforward button
          fastForwardBounds =
              new Rectangle(
                  playPauseBounds.x + playPauseBounds.width + BUTTON_OFFSET_X,
                  previousBounds.y,
                  BUTTON_WIDTH,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(
              fastForwardBounds.x,
              fastForwardBounds.y,
              fastForwardBounds.width,
              fastForwardBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              fastForwardBounds.x + 1,
              fastForwardBounds.y + 1,
              fastForwardBounds.width - 1,
              fastForwardBounds.height - 1);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          shapeHeight = fastForwardBounds.height - 3;
          shapeX =
              fastForwardBounds.x
                  + (int) (((float) fastForwardBounds.width) / 2.0)
                  - (int) ((float) shapeHeight / 2.0);
          drawPlayerControlShape(g2, shapeX, fastForwardBounds.y + 2, shapeHeight, "fastforward");

          if (MouseHandler.inBounds(fastForwardBounds) && MouseHandler.mouseClicked) {
            Replay.controlPlayback("ff_plus");
          }

          // next button
          nextBounds =
              new Rectangle(
                  fastForwardBounds.x + fastForwardBounds.width + BUTTON_OFFSET_X,
                  previousBounds.y,
                  BUTTON_WIDTH,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(nextBounds.x, nextBounds.y, nextBounds.width, nextBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              nextBounds.x + 1, nextBounds.y + 1, nextBounds.width - 1, nextBounds.height - 1);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          shapeHeight = nextBounds.height - 3;
          shapeX =
              nextBounds.x
                  + (int) (((float) nextBounds.width) / 2.0)
                  - (int) ((float) shapeHeight / 2.0);
          drawPlayerControlShape(g2, shapeX, nextBounds.y + 2, shapeHeight, "next");

          if (MouseHandler.inBounds(nextBounds) && MouseHandler.mouseClicked) {
            ReplayQueue.skipped = true;
            ReplayQueue.nextReplay();
          }

          // open queue button (right aligned)
          queueBounds =
              new Rectangle(
                  barBounds.x + barBounds.width - BUTTON_WIDTH * 2,
                  previousBounds.y,
                  BUTTON_WIDTH * 2,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(queueBounds.x, queueBounds.y, queueBounds.width, queueBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              queueBounds.x + 1, queueBounds.y + 1, queueBounds.width - 1, queueBounds.height - 1);
          g2.setColor(color_text);
          setAlpha(g2, 1.0f);

          shapeHeight = queueBounds.height - 3;
          shapeX = queueBounds.x + 3;
          drawPlayerControlShape(g2, shapeX, queueBounds.y + 2, shapeHeight, "queue");

          drawShadowText(
              g2,
              "Queue",
              queueBounds.x + BUTTON_OFFSET_X + (int) (shapeHeight * 2),
              queueBounds.y + queueBounds.height - 2,
              color_white,
              false);

          if (MouseHandler.inBounds(queueBounds) && MouseHandler.mouseClicked) {
            Launcher.getQueueWindow().showQueueWindow();
          }

          // stop button
          stopBounds =
              new Rectangle(
                  queueBounds.x - BUTTON_WIDTH - BUTTON_OFFSET_X,
                  previousBounds.y,
                  BUTTON_WIDTH,
                  BUTTON_HEIGHT);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          g2.drawRect(stopBounds.x, stopBounds.y, stopBounds.width, stopBounds.height);
          g2.setColor(color_shadow);
          setAlpha(g2, 0.50f);
          g2.fillRect(
              stopBounds.x + 1, stopBounds.y + 1, stopBounds.width - 1, stopBounds.height - 1);

          g2.setColor(color_text);
          setAlpha(g2, 1.0f);
          shapeHeight = stopBounds.height - 3;
          shapeX =
              stopBounds.x
                  + (int) (((float) stopBounds.width) / 2.0)
                  - (int) ((float) shapeHeight / 2.0);
          drawPlayerControlShape(g2, shapeX, stopBounds.y + 2, shapeHeight, "stop");

          if (MouseHandler.inBounds(stopBounds) && MouseHandler.mouseClicked) {
            Replay.controlPlayback("stop");
          }
        }
      }
    }

    // Draw software cursor
    if (screenshot || Settings.SOFTWARE_CURSOR.get(Settings.currentProfile)) {
      setAlpha(g2, 1.0f);
      g2.drawImage(image_cursor, MouseHandler.x, MouseHandler.y, null);
    }

    g2.dispose();

    // Right now is a good time to take a screenshot if one is requested
    if (screenshot) {
      try {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH.mm.ss");
        String fname =
            Settings.Dir.SCREENSHOT + "/" + "Screenshot from " + format.format(new Date()) + ".png";
        File screenshotFile = new File(fname);
        ImageIO.write(game_image, "png", screenshotFile);
        if (!quietScreenshot)
          Client.displayMessage(
              "@cya@Screenshot saved to '" + screenshotFile.toString() + "'", Client.CHAT_NONE);
      } catch (Exception e) {
      }
      screenshot = false;
    }

    g.drawImage(game_image, 0, 0, null);

    frames++;
    time = System.currentTimeMillis();
    if (time > fps_timer) {
      fps = frames;
      frames = 0;
      fps_timer = time + 1000;
    }

    if (width != new_size.width || height != new_size.height) handle_resize();
    if (Settings.fovUpdateRequired) {
      Camera.setFoV(Settings.FOV.get(Settings.currentProfile));
      Settings.fovUpdateRequired = false;
    }

    // Reset the mouse click handler
    MouseHandler.mouseClicked = false;
  }

  public static void drawBar(
      Graphics2D g, Image image, int x, int y, Color color, float alpha, int value, int total) {
    // Prevent divide by zero
    if (total == 0) return;

    int width = image.getWidth(null) - 2;
    int percent = value * width / total;

    g.setColor(color_shadow);
    g.fillRect(x + 1, y, width, image.getHeight(null));

    g.setColor(color);
    setAlpha(g, alpha);
    g.fillRect(x + 1, y, percent, image.getHeight(null));
    setAlpha(g, 1.0f);

    g.drawImage(image_bar_frame, x, y, null);
    drawShadowText(
        g,
        value + "/" + total,
        x + (image.getWidth(null) / 2),
        y + (image.getHeight(null) / 2) - 2,
        color_text,
        true);
  }

  public static void setAlpha(Graphics2D g, float alpha) {
    g.setComposite(AlphaComposite.SrcOver.derive(alpha));
  }

  public static boolean stringIsWithinList(String input, ArrayList<String> items) {
    if (items.size() <= 0) {
      return false;
    }
    Iterator it = items.iterator();
    while (it.hasNext()) {
      String item = String.valueOf(it.next());
      if (item.trim().length() > 0
          && input.trim().toLowerCase().contains(item.trim().toLowerCase())) {
        return true;
      }
    }
    return false;
  }

  public static void drawHighlighImage(Graphics2D g, String text, int x, int y) {
    int correctedX = x;
    int correctedY = y;
    // Adjust for centering
    Dimension bounds = getStringBounds(g, text);
    correctedX -= (bounds.width / 2);
    correctedY += (bounds.height / 2);
    g.drawImage(image_highlighted_item, correctedX - 15, correctedY - 10, null);
  }

  public static void drawShadowText(
      Graphics2D g, String text, int x, int y, Color textColor, boolean center) {
    int textX = x;
    int textY = y;
    if (center) {
      Dimension bounds = getStringBounds(g, text);
      textX -= (bounds.width / 2);
      textY += (bounds.height / 2);
    }

    g.setColor(color_shadow);
    g.drawString(text, textX + 1, textY);
    g.drawString(text, textX - 1, textY);
    g.drawString(text, textX, textY + 1);
    g.drawString(text, textX, textY - 1);

    g.setColor(textColor);
    g.drawString(text, textX, textY);
  }

  public static void drawColoredText(Graphics2D g, String text, int x, int y) {
    int textX = x;
    int textY = y;

    String outputText = "";
    Color outputColor = colorFromCode("@yel@");
    Color currentColor = outputColor;
    for (int i = 0; i < text.length(); i++) {
      if (text.charAt(i) == '@' && text.charAt(i + 4) == '@') {
        outputColor = colorFromCode(text.substring(i, i + 4));
        i += 5;
        if (i >= text.length()) break;
      }

      if (currentColor != outputColor) {
        if (outputText.length() > 0) {
          g.setColor(color_shadow);
          g.drawString(outputText, textX + 1, textY);
          g.drawString(outputText, textX - 1, textY);
          g.drawString(outputText, textX, textY + 1);
          g.drawString(outputText, textX, textY - 1);

          g.setColor(currentColor);
          g.drawString(outputText, textX, textY);
          textX += getStringBounds(g, outputText).width;
        }
        currentColor = outputColor;
        outputText = "";
      }

      outputText += text.charAt(i);
    }

    g.setColor(color_shadow);
    g.drawString(outputText, textX + 1, textY);
    g.drawString(outputText, textX - 1, textY);
    g.drawString(outputText, textX, textY + 1);
    g.drawString(outputText, textX, textY - 1);

    g.setColor(currentColor);
    g.drawString(outputText, textX, textY);
  }

  public static void drawShadowTextBorder(
      Graphics2D g,
      String text,
      int x,
      int y,
      Color textColor,
      float alpha,
      float boxAlpha,
      boolean border,
      int borderSize) {
    int textX = x;
    int textY = y;
    Dimension bounds = getStringBounds(g, text);
    textX -= (bounds.width / 2);
    textY += (bounds.height / 2);

    g.setColor(color_shadow);
    int rectX = x - (bounds.width / 2) - 2 - borderSize;
    int rectY = y - (bounds.height / 2) + 2 - borderSize;
    int rectWidth = bounds.width + 2 + (borderSize * 2);
    int rectHeight = bounds.height + (borderSize * 2);
    if (border) {
      setAlpha(g, 1.0f);
      g.drawRect(rectX, rectY, rectWidth, rectHeight);
    }
    setAlpha(g, boxAlpha);
    g.fillRect(rectX, rectY, rectWidth, rectHeight);
    setAlpha(g, alpha);
    g.drawString(text, textX + 1, textY);
    g.drawString(text, textX - 1, textY);
    g.drawString(text, textX, textY + 1);
    g.drawString(text, textX, textY - 1);

    g.setColor(textColor);
    g.drawString(text, textX, textY);
  }

  // rather than import someone else's font and try to get the unicode to work,
  // just draw the classic player control shapes myself. they're not that complex.
  public static void drawPlayerControlShape(Graphics2D g, int x, int y, int height, String icon) {
    int nPoints;
    int[] xPoints;
    int[] yPoints;
    int halfx = (int) (x + (float) height / 2.0);
    int halfy = (int) (y + (float) height / 2.0);

    switch (icon) {
      case "fastforward": // ⏩
        // this  icon is two right angle triangles touching
        nPoints = 7;
        xPoints = new int[nPoints];
        yPoints = new int[nPoints];

        // define shape
        xPoints[0] = x;
        yPoints[0] = y;

        xPoints[1] = halfx;
        yPoints[1] = halfy;

        xPoints[2] = halfx;
        yPoints[2] = y;

        xPoints[3] = x + height;
        yPoints[3] = halfy;

        xPoints[4] = halfx;
        yPoints[4] = y + height;

        xPoints[5] = halfx;
        yPoints[5] = halfy;

        xPoints[6] = x;
        yPoints[6] = y + height;

        // goes back to x,y to close shape by itself
        g.fillPolygon(xPoints, yPoints, nPoints);
        break;
      case "slowforward": // ⏪
        // if I was clever, I'd do some math on "fastforward"'s
        // xPoints coordinates to generate slowforward.
        // but it's simple enough to just manually swap "x" & "x+height"
        // faster to execute too...
        nPoints = 7;
        xPoints = new int[nPoints];
        yPoints = new int[nPoints];

        xPoints[0] = x + height;
        yPoints[0] = y;

        xPoints[1] = halfx;
        yPoints[1] = halfy;

        xPoints[2] = halfx;
        yPoints[2] = y;

        xPoints[3] = x;
        yPoints[3] = halfy;

        xPoints[4] = halfx;
        yPoints[4] = y + height;

        xPoints[5] = halfx;
        yPoints[5] = halfy;

        xPoints[6] = x + height;
        yPoints[6] = y + height;

        // goes back to x,y to close shape by itself
        g.fillPolygon(xPoints, yPoints, nPoints);
        break;
      case "next": // ⏭
        // this is just fastforward but with a line drawn next  to it
        // the line should be 1 px width;
        drawPlayerControlShape(g, x, y, height, "fastforward");
        g.drawLine(x + height, y, x + height, y + height - 1);
        break;
      case "previous": // ⏮
        drawPlayerControlShape(g, x, y, height, "slowforward");
        g.drawLine(x, y, x, y + height - 1);
        break;
      case "playpause": // ⏯
        // pause's white space is as wide as single line of pause
        // each element in pause is 1/3rd width of triangle
        nPoints = 3;
        xPoints = new int[nPoints];
        yPoints = new int[nPoints];

        xPoints[0] = x;
        yPoints[0] = y;

        xPoints[1] = halfx;
        yPoints[1] = halfy;

        xPoints[2] = x;
        yPoints[2] = y + height;

        g.fillPolygon(xPoints, yPoints, nPoints); // triangle-y part

        // now draw pause symbol next to it
        g.fillRect(halfx, y, (int) ((float) height / 6.0), height);
        g.fillRect(
            (int) (x + height - ((float) height / 6.0)), y, (int) ((float) height / 6.0), height);
        break;
      case "stop": // ⏹
        // just a rectangle. :)
        // slightly smaller than height
        g.fillRect(
            x + (int) (height * 0.15),
            y + (int) (height * 0.15),
            (int) (height * 0.80),
            (int) (height * 0.80));
        break;
      case "queue":
        // this shape looks like ":=" but with an extra dot, and an extra line
        int right_bit_width = (int) ((float) height * 1.2);
        int line_size = (int) ((float) height / 5.0) + 1;
        for (int i = 0; i < 3; i++) {
          g.fillRect(x, y + (int) (i * line_size * 1.5), line_size, line_size); // left dot
          g.fillRect(
              x + line_size * 2,
              y + (int) (i * line_size * 1.5),
              right_bit_width,
              line_size); // right rectangle
        }
        break;

      default:
        Logger.Debug("drawPlayerControlShape given invalid shape");
    }
  }

  public static void takeScreenshot(boolean quiet) {
    quietScreenshot = quiet;
    screenshot = true;
  }

  private static String fixLengthString(String string) {
    for (int i = 0; i < string.length(); i++) {
      if (string.charAt(i) == '~' && string.charAt(i + 4) == '~') {
        String coord = string.substring(i + 1, 3);
        string = string.replace(coord, "0" + coord);
      }
    }
    return string;
  }

  private static Dimension getStringBounds(Graphics2D g, String str) {
    FontRenderContext context = g.getFontRenderContext();
    Rectangle2D bounds = g.getFont().getStringBounds(str, context);
    return new Dimension((int) bounds.getWidth(), (int) bounds.getHeight());
  }

  private static Color colorFromCode(String s) {
    int hexCode = 0xffffff;

    if (s.substring(1, 4).equalsIgnoreCase("red")) hexCode = 0xff0000;
    else if (s.substring(1, 4).equalsIgnoreCase("lre")) hexCode = 0xff9040;
    else if (s.substring(1, 4).equalsIgnoreCase("yel")) hexCode = 0xffff00;
    else if (s.substring(1, 4).equalsIgnoreCase("gre")) hexCode = 65280;
    else if (s.substring(1, 4).equalsIgnoreCase("blu")) hexCode = 255;
    else if (s.substring(1, 4).equalsIgnoreCase("cya")) hexCode = 65535;
    else if (s.substring(1, 4).equalsIgnoreCase("mag")) hexCode = 0xff00ff;
    else if (s.substring(1, 4).equalsIgnoreCase("whi")) hexCode = 0xffffff;
    else if (s.substring(1, 4).equalsIgnoreCase("bla")) hexCode = 0;
    else if (s.substring(1, 4).equalsIgnoreCase("dre")) hexCode = 0xc00000;
    else if (s.substring(1, 4).equalsIgnoreCase("ora")) hexCode = 0xff9040;
    else if (s.substring(1, 4).equalsIgnoreCase("ran")) hexCode = (int) (Math.random() * 16777215D);
    else if (s.substring(1, 4).equalsIgnoreCase("or1")) hexCode = 0xffb000;
    else if (s.substring(1, 4).equalsIgnoreCase("or2")) hexCode = 0xff7000;
    else if (s.substring(1, 4).equalsIgnoreCase("or3")) hexCode = 0xff3000;
    else if (s.substring(1, 4).equalsIgnoreCase("gr1")) hexCode = 0xc0ff00;
    else if (s.substring(1, 4).equalsIgnoreCase("gr2")) hexCode = 0x80ff00;
    else if (s.substring(1, 4).equalsIgnoreCase("gr3")) hexCode = 0x40ff00;

    return new Color(hexCode);
  }

  private static void drawNPCBar(Graphics2D g, int x, int y, NPC npc) {
    Dimension bounds = new Dimension(173, 40);
    float hp_ratio = (float) (npc.currentHits) / (float) (npc.maxHits);

    // Container
    setAlpha(g, 0.5f);
    g.setColor(color_gray);
    g.fillRect(x - 1, y - 1, bounds.width + 2, bounds.height + 2);
    g.setColor(color_shadow);
    g.fillRect(x, y, bounds.width, bounds.height);

    // HP bar
    setAlpha(g, 1.0f);
    g.setColor(new Color(99, 20, 19));
    g.fillRect(x, y + 20, bounds.width, bounds.height / 2);
    g.setColor(new Color(10, 134, 51));
    g.fillRect(x, y + 20, (int) (bounds.width * hp_ratio), bounds.height / 2);

    // HP text
    if (Settings.NPC_HEALTH_SHOW_PERCENTAGE.get(Settings.currentProfile))
      drawShadowText(
          g,
          (int) Math.ceil(hp_ratio * 100) + "%",
          x + (bounds.width / 2),
          y + (bounds.height / 2) + 8,
          color_text,
          true);
    else
      drawShadowText(
          g,
          npc.currentHits + "/" + npc.maxHits,
          x + (bounds.width / 2),
          y + (bounds.height / 2) + 8,
          color_text,
          true);

    // NPC name
    drawShadowText(
        g, npc.name, x + (bounds.width / 2), y + (bounds.height / 2) - 12, color_text, true);
  }
}

class ItemComparator implements Comparator<Item> {

  @Override
  public int compare(Item a, Item b) {
    // this is reverse alphabetical order b/c we display them/in reverse order (y-=12 ea item)
    int offset = a.getName().compareToIgnoreCase(b.getName()) * -1;
    if (offset > 0) { // item a is alphabetically before item b
      offset = 10;
    } else if (offset < 0) { // item b is alphabetically before item a
      offset = -10;
      // items have the same name we would like to group items that are on the same tile as well,
      // not just having
      // the same name, so that we can use "last_item" in a useful way
    } else {
      if (a.x == b.x && a.y == b.y) {
        offset = 0; // name is the same and so is location, items are considered equal
      } else {
        if (a.x < b.x) {
          offset = -5;
        } else {
          offset = 5;
        }
      }
    }
    return offset;
  }
}