public final class Client extends ClientFrame {
	private static final Logger log = LoggerFactory.getLogger(Client.class);

	public static final String DEFAULT_HOST = "localhost";
	public static final int DEFAULT_PORT = 36954;
	public static final int[] ICON_SIZES = {128, 64, 32, 16};
	public static final int DEFAULT_WIDTH = 800;
	public static final int DEFAULT_HEIGHT = 600;

	protected static final Options options;

	static {
		options = new Options();

		options.addOption("h", "help", false, "print this help.");
		options.addOption("v", "disable-vsync", false, "disable vsync, default false.");
		options.addOption("p", "port", true, "server port number to connect to, default " + DEFAULT_PORT + ".");
		options.addOption("s", "server", true, "server hostname to connect to, default " + DEFAULT_HOST + ".");
	}

	public static final void main(String[] args) {
		try {
			CommandLineParser parser = new PosixParser();
			CommandLine config = parser.parse(options, args);

			if (config.hasOption("h")) {
				HelpFormatter help = new HelpFormatter();
				help.printHelp("java " + Client.class.getSimpleName(), options);
				return;
			}

			Client client = new Client(config);
			client.run();
		}
		catch (ParseException e) {
			log.error("Error parsing command line options: " + e);
		}
		catch (RuntimeException e) {
			log.error(e.getMessage());
		}
	}

	protected static BufferedImage[] loadIcons(String name, int[] sizes) {
		BufferedImage[] iconImages = new BufferedImage[sizes.length];
		for (int i = 0;i < iconImages.length;i++) {
			try {
				URL resource = Client.class.getResource(name + sizes[i] + ".png");
				if (resource == null) {
					// fatal error
					throw new RuntimeException("Resource not found: icon" + name + sizes[i] + ".png");
				}

				iconImages[i] = ImageIO.read(resource);
			}
			catch (IOException e) {
				// fatal error
				throw new RuntimeException("Error loading icon: " + e);
			}
		}

		return iconImages;
	}

	protected LoginWindow loginWindow;
	protected final CommandLine config;
	protected final Connection connection;
	protected final WorldManager world;
	protected final Camera camera;
	protected final HUD hud;
	protected final PublicKey publicKey;

	public Client(CommandLine config) {
		super ("Test Client", DEFAULT_WIDTH, DEFAULT_HEIGHT, Client.loadIcons("icon", ICON_SIZES));

		this.config = config;

		if (config.hasOption("v"))
			super.setVSync(false);

		loginWindow = new LoginWindow(this);
		connection = new Connection(this);
		world = new WorldManager(this);
		camera = world.getCamera();
		hud = new HUD(this);

		publicKey = (PublicKey) PersistenceManager.load(Client.class.getResource("publickey.xml"));

		// Pre-load the item definitions
		Item.load();

		log.info("New client started");
	}

	public HUD getHUD() {
		return hud;
	}

	public Connection getConnection() {
		return connection;
	}

	public WorldManager getWorldManager() {
		return world;
	}

	public boolean isLoggedIn() {
		return loginWindow == null;
	}

	public void login(final String username, final String password) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				Random random = new Random();

				String hostname = config.getOptionValue("s", DEFAULT_HOST);
				int port = DEFAULT_PORT;
				if (config.hasOption("p")) {
					try {
						port = Integer.parseInt(config.getOptionValue("p"));
					}
					catch (NumberFormatException e) {
						log.error("Invalid port number: " + config.getOptionValue("p") + ", trying default: " + port);
					}
				}

				try {
					connection.open(hostname, port);

					Hash usernameHash = new Hash(username.toLowerCase());// Clean the username and hash it to get the users ID
					Hash passwordHash = new Hash(usernameHash + password); // Salt the password and hash it

					PacketBuilder packet = new PacketBuilder(Packet.Type.LOGIN_SEND);

					packet.putHash(usernameHash);
					packet.putHash(passwordHash);

					long encryptionSeed = random.nextLong();
					packet.putLong(encryptionSeed);

					long decryptionSeed = random.nextLong();
					packet.putLong(decryptionSeed);

					// Encrypt the login packet with our public key
					packet.encrypt(publicKey);

					connection.write(packet);

					// from now on we want to encrypt outgoing packets
					connection.enableEncryption(encryptionSeed, decryptionSeed);
				}
				catch (RuntimeIoException rioe) {
					connection.close();

					log.warn("Failed to connect to: " + hostname + ":" + port);
					loginWindow.failed("Failed to connect to the game server.");
				}
			}
		}).start();
	}

	public void loginSuccess(PlayerProfile profile) {
		world.init(profile);
		loginWindow = null;

		log.info("Successfully logged in.");
	}

	public void loginFailed(String message) {
		loginWindow.failed(message);
		connection.close();
	}

	public void handleLogout() {
		if (loginWindow != null)
			return;

		connection.close();
		loginWindow = new LoginWindow(this);
	}

	@Override
	protected void update(long now) {
		// Update the connection
		connection.update(now);

		// If there is a login window, update it
		if (loginWindow != null) {
			loginWindow.update(now);
			return;
		}

		// Update the world
		world.update(now);

		// Update the HUD
		hud.update(now);
	}

	@Override
	public void display(Graphics g) {
		Graphics2D g2d = g.get2D();

		// If there is a login window, display it
		if (loginWindow != null) {
			g2d.begin();
			{
				loginWindow.display(g2d);
			}
			g2d.end();

			return;
		}

		Graphics3D g3d = g.get3D();

		g3d.begin(world.getLocation(), camera.getZoom(), camera.getRotation());
		{
			// Display the world
			world.display(g3d);
		}
		g3d.end();

		g2d.begin();
		{
			// Draw the HUD on-top of the world
			hud.display(g2d);
		}
		g2d.end();
	}

	@Override
	public void mouseClicked(int x, int y, boolean left) {
		// If there is a login window, it was clicked
		if (loginWindow != null) {
			loginWindow.mouseClicked(x, y, left);
			return;
		}

		// If the HUD was clicked, don't pass the key to the world
		if (hud.mouseClicked(x, y, left))
			return;

		// Pass the click to the world
		world.mouseClicked(x, y, left);
	}

	@Override
	public void keyPressed(int keyCode, char keyChar) {
		// If there is a login window, the key goes there
		if (loginWindow != null) {
			loginWindow.keyPressed(keyCode, keyChar);
			return;
		}

		// Send the key to the HUD
		hud.keyPressed(keyCode, keyChar);
	}

	@Override
	protected void close() {
		connection.destroy();
	}
} public class WorldManager extends GameEngine {

	protected final Connection connection;

	protected PlayerProfile profile;

	public WorldManager(Client client) {
		super (client);

		connection = client.getConnection();
	}

	public Hash getID() {
		return profile.id;
	}

	public String getUsername() {
		return profile.username;
	}

	public FriendList getFriends() {
		return profile.friends;
	}

	public StatList getStats() {
		return profile.stats;
	}

	public Inventory getInventory() {
		return profile.inventory;
	}

	public void init(PlayerProfile profile) {
		this.profile = profile;

		client.setUpdateRate(ClientFrame.UPDATE_RATE_MAX);

		Model model = client.getGraphics().loadModel("test.ms3d", MilkShapeModel.class);
		model.setScale(0.03f);

		super.init(new Player(profile.id, profile.username, profile.location, model));
	}

	public void sendFriendAdd(String username) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.FRIEND_ADD_SEND);

		packet.putString(username);

		connection.write(packet);
	}

	public void sendFriendRemove(Friend friend) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.FRIEND_REMOVE_SEND);

		packet.putHash(friend.getID());

		connection.write(packet);
	}

	public void sendFriendMessage(Friend friend, String message) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.FRIEND_MESSAGE_SEND);

		packet.putHash(friend.getID());
		packet.putString(message);

		connection.write(packet);
	}

	public void sendUseItem(int index) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.USE_ITEM_SEND);

		packet.putByte((byte) index);

		connection.write(packet);
	}

	public void sendChat(String message) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.CHAT_SEND);

		packet.putString(message);

		connection.write(packet);
	}

	public void sendWalkTo(Path path) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.WALK_TO_SEND);

		packet.putPath(path);

		connection.write(packet);
	}
} public class Friends extends Menu implements ActionListener {
	public static final Color FRIEND_ONLINE_COLOR = Color.GREEN;
	public static final Color FRIEND_OFFLINE_COLOR = Color.WHITE;

	protected final WorldManager world;
	protected final HUD hud;
	protected final Mouse mouse;
	protected TextField inputField;
	protected Friend activeFriend;

	public Friends(WorldManager world, HUD hud, Mouse mouse) {
		super("Friends", 140, 200);

		this.world = world;
		this.hud = hud;
		this.mouse = mouse;

		inputField = null;
		activeFriend = null;
	}

	@Override
	public void onUpdate(long now) {
		if (inputField != null && !hud.hasInputBox())
			inputField = null;
	}

	@Override
	public void onMouseClicked(int x, int y, boolean left) {
		// If no friend is active, or the message Input is overriding
		if (activeFriend == null || inputField != null)
			return;

		if (left) {
			if (!activeFriend.isOnline()) {
				hud.addMessage(activeFriend.getUsername() + " is not currently online.", ChatMessages.Type.MESSAGE);
				return;
			}

			// Pop up an input box for the PM
			inputField = hud.getInputBox("Send to " + activeFriend.getUsername());
			inputField.addActionListener(this);

			return;
		}

		// TODO: Menu with remove from friends option
		System.out.println("Show menu for: " + activeFriend);
	}

	@Override
	public void onDisplay(Graphics2D g, int x, int y) {
		y += 4;

		this.setActiveFriend(null);

		FriendList friends = world.getFriends();
		for (Friend friend : friends) {
			Color color = friend.isOnline() ? FRIEND_ONLINE_COLOR : FRIEND_OFFLINE_COLOR;

			if (mouse.getX() > x && mouse.getX() < (x + super.width) && mouse.getY() >= y && mouse.getY() < (y + g.getFontHeight())) {
				this.setActiveFriend(friend);
				color = Color.RED;
			}

			y += g.getFontHeight();

			g.drawString(friend.getUsername(), x + 6, y, color, true);
		}
	}

	private void setActiveFriend(Friend activeFriend) {
		if (inputField != null)
			return;

		this.activeFriend = activeFriend;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		if (e.getSource() == inputField) {
			String message = inputField.getText();
			if (!message.isEmpty()) {
				world.sendFriendMessage(activeFriend, message);
				hud.addMessage(world.getUsername() + ": " + message, ChatMessages.Type.PRIVATE_CHAT);
			}

			hud.destroyInputBox();
		}
	}
} public class Inventory extends Menu {
	public static final int BOX_WIDTH = 48;
	public static final int BOX_HEIGHT = 36;

	public static final int WIDTH = 5;
	public static final int HEIGHT = 8;

	public static final Color EQUIPED_COLOR = new Color(0.7f, 0, 0, 0.6f);

	protected final WorldManager world;
	protected final NumberFormat formatter;
	protected final Map<String, Sprite> sprites;

	public Inventory(WorldManager world, Graphics g) {
		super("Inventory", BOX_WIDTH * WIDTH, BOX_HEIGHT * HEIGHT);

		this.world = world;

		formatter = new DecimalFormat("#,###,###");
		sprites = g.loadSpritePack("items.zip");
	}

	@Override
	public void onUpdate(long now) { }

	@Override
	public void onMouseClicked(int x, int y, boolean left) {
		x /= BOX_WIDTH;
		y /= BOX_HEIGHT;

		int index = (y * WIDTH) + x;

		// If we clicked an empty box, ignore it
		if (index >= world.getInventory().size())
			return;

		// If it was a left click, use that item
		if (left)
			world.sendUseItem(index);
		// If it was a right click, handle the action menu
		else {
			// TODO: Handle the action menu for item <index>
			System.out.println("Show menu for: " + world.getInventory().get(index));
		}
	}

	@Override
	public void onDisplay(Graphics2D g, int x, int y) {
		int index = 0;
		Point offset = new Point(x, y);

		for (int by = 0;by < HEIGHT;by++) {
			offset.x = x;

			for (int bx = 0;bx < WIDTH;bx++) {
				Item item = world.getInventory().get(index++);

				// If we have an item, draw it
				if (item != null) {
					// If its equiped draw the background
					if (item.isEquiped())
						g.fillRect(offset.x, offset.y, BOX_WIDTH, BOX_HEIGHT, EQUIPED_COLOR);

					// Draw the actual item
					Sprite sprite = sprites.get(item.getID() + ".png");
					if (sprite != null)
						g.drawSprite(sprite, offset.x, offset.y, BOX_WIDTH, BOX_HEIGHT);

					// If its stackable, draw the amount
					if (item.isStackable())
						g.drawString(formatter.format(item.getAmount()), offset.x + 2, offset.y + g.getFontHeight(), Color.YELLOW, true);
				}

				if (super.borderColor != null)
					g.drawRect(offset.x, offset.y, BOX_WIDTH, BOX_HEIGHT, super.borderColor);

				offset.x += BOX_WIDTH;
			}

			offset.y += BOX_HEIGHT;
		}
	}
} public class Magic extends Menu {

	protected final WorldManager world;

	public Magic(WorldManager world) {
		super("Magic", 140, 200);

		this.world = world;
	}

	@Override
	public void onUpdate(long now) { }

	@Override
	public void onMouseClicked(int x, int y, boolean left) {
		// TODO Auto-generated method stub
	}

	@Override
	public void onDisplay(Graphics2D g, int x, int y) {
		// TODO: display stuff
	}
} public class MiniMap extends Menu {
	public static final int MAP_SIZE = 128;

	public static final int TRIPLE_MAP_SIZE;
	public static final int HALF_MAP_SIZE;
	public static final int TRIPLE_HALF_MAP_SIZE;

	static {
		TRIPLE_MAP_SIZE = MAP_SIZE * 3;
		HALF_MAP_SIZE = MAP_SIZE / 2;
		TRIPLE_HALF_MAP_SIZE = HALF_MAP_SIZE * 3;
	}

	protected final WorldManager world;
	protected final Camera camera;
	protected final LocalMap map;
	protected final EditableSprite mapSprite;

//	protected final BufferedImage outerImage;
//	protected final Graphics2D outerImageGraphics;

	protected Point location;
	protected int rotation;

	public MiniMap(WorldManager world, Graphics graphics) {
		super("World Map", TRIPLE_HALF_MAP_SIZE, TRIPLE_HALF_MAP_SIZE);
		super.setBackgroundColor(null);

		this.world = world;

		camera = world.getCamera();
		map = world.getMap();

		mapSprite = new EditableSprite(TRIPLE_MAP_SIZE, TRIPLE_MAP_SIZE, false, graphics);

//		outerImage = new BufferedImage(TRIPLE_HALF_MAP_SIZE, TRIPLE_HALF_MAP_SIZE, BufferedImage.TYPE_INT_ARGB);
//		outerImageGraphics = outerImage.createGraphics();
//		outerImageGraphics.setBackground(null);

		location = null;
		rotation = 0;
	}

	@Override
	public void onUpdate(long now) {
		location = world.getLocation();
		rotation = camera.getRotation();

		Point start = new Point(location.x - HALF_MAP_SIZE, location.y - HALF_MAP_SIZE);

		// draw all the map tiles
		for (int y = 0;y < MAP_SIZE;y++) {
			for (int x = 0;x < MAP_SIZE;x++) {
				LocalTile t = map.getTile(start.x + x, start.y + y);
				Color c = (t == null) ? Color.BLACK : Color.GREEN; // TODO: Tile color

				mapSprite.fillRect(x * 3, y * 3, 3, 3, c);
			}
		}

		// draw other players on the map
		for (Player player : world.getPlayers()) {
			int x = player.getLocation().x - start.x;
			int y = player.getLocation().y - start.y;

			// If the player is outside the map, skip them
			if (x < 0 || x >= MAP_SIZE || y < 0 || y >= MAP_SIZE)
				continue;

			mapSprite.fillDiamond(x * 3, y * 3, 3, world.getFriends().contains(player.getID()) ? Color.GREEN : Color.WHITE);
		}

		// draw ourself on the map
		mapSprite.fillDiamond(TRIPLE_HALF_MAP_SIZE, TRIPLE_HALF_MAP_SIZE, 5, Color.WHITE);

		// Update the texture
		mapSprite.flush();

//		AffineTransform transform = new AffineTransform();
//		transform.rotate(Math.toRadians(rotation), outerImage.getWidth() / 2, outerImage.getHeight() / 2);
//		transform.translate(-(outerImage.getWidth() / 2), -(outerImage.getHeight() / 2));

//		outerImageGraphics.clearRect(0, 0, outerImage.getWidth(), outerImage.getHeight());
//		outerImageGraphics.drawImage(innerImage.getImage(), transform, null);
	}

	@Override
	public void onMouseClicked(int x, int y, boolean left) {
		// Right clicks do nothing
		if (!left || location == null)
			return;

		Point target = this.calcClickTarget(x, y);
		// If we clicked on ourselves, do nothing
		if (target.equals(Point.ZERO))
			return;

		target.x += location.x;
		target.y += location.y;

		Path path = world.getMap().generatePath(location, target);
		// There is no path to this location
		if (path == null)
			return;

		// Send a walk request
		world.sendWalkTo(path);
	}

	private Point calcClickTarget(int x, int y) {
		x = ((x / 3) * 2) - HALF_MAP_SIZE;
		y = ((y / 3) * 2) - HALF_MAP_SIZE;

		switch (rotation) {
		// 0<b0>, no translation
		case 0: {
			return new Point(x / 2, y / 2);
		}

		// 90<b0>, swap x and y, flip y
		case 90: {
			return new Point(y / 2, -x / 2);
		}

		// 180<b0>, flip x and y
		case 180: {
			return new Point(-x / 2, -y / 2);
		}

		// 270<b0>, swap x and y, flip x
		case 270: {
			return new Point(-y / 2, x / 2);
		}

		// Not perfectly aligned - we need to calculate the target using trigonometry
		default: {
			// Calculate the x, y offset of where was clicked - this is our adjacent and opposite sides
			double adj = x;
			double opp = y;

			double angle;
			double hyp;

			// If the opposite is 0
			if (y == 0) {
				angle = 0;
				hyp = adj;
			}
			// Calculate the angle, and then hypotenuse
			else {
				angle = Math.atan(opp / adj);
				hyp = opp / Math.sin(angle);
			}

			// Add the rotation on to our angle
			angle += (2 * Math.PI) - Math.toRadians(rotation);

			// Calculate the new adjacent and opposite, these are our translated x and y offsets
			adj = hyp * Math.cos(angle);
			opp = hyp * Math.sin(angle);

			return new Point((int) Math.round(adj / 2), (int) Math.round(opp / 2));
		}
		}
	}

	@Override
	public void onDisplay(Graphics2D g, int x, int y) {
		g.drawSprite(mapSprite, x, y, TRIPLE_HALF_MAP_SIZE, TRIPLE_HALF_MAP_SIZE);
	}
} public class Options extends Menu {

	protected final WorldManager world;

	public Options(WorldManager world) {
		super("Options", 140, 200);

		this.world = world;
	}

	@Override
	public void onUpdate(long now) { }

	@Override
	public void onMouseClicked(int x, int y, boolean left) {
		// TODO Auto-generated method stub
	}

	@Override
	public void onDisplay(Graphics2D g, int x, int y) {
		// TODO: display stuff
	}
} public class PlayerStats extends Menu {
	public static final int STAT_X_OFFSET = 60;

	protected final WorldManager world;
	protected final Mouse mouse;
	protected final NumberFormat formatter;
	protected Stat.Type activeStat;

	public PlayerStats(WorldManager world, Graphics g, Mouse mouse) {
		super("Player Stats", STAT_X_OFFSET + 52, 36 + ((StatList.NUM_STATS + 4) * g.get2D().getFontHeight()));

		this.world = world;
		this.mouse = mouse;

		formatter = new DecimalFormat("#,###,###");
		activeStat = null;
	}

	@Override
	public void onUpdate(long now) { }

	@Override
	public void onMouseClicked(int x, int y, boolean left) { }

	@Override
	public void onDisplay(Graphics2D g, int x, int y) {
		y += 4;

		activeStat = null;

		// Display the stats
		for (Stat.Type type : Stat.Type.values()) {
			Stat stat = world.getStats().get(type);
			Color color = Color.WHITE;

			if (mouse.getX() > x && mouse.getX() < (x + super.width) && mouse.getY() >= y && mouse.getY() < (y + g.getFontHeight())) {
				activeStat = type;
				color = Color.RED;
			}

			y += g.getFontHeight();

			g.drawString(type.name, x + 12, y, color, true);
			g.drawString(stat.toString(), x + 12 + STAT_X_OFFSET, y, color, true);

		}

		y += 8;
		g.drawLine(x, y, x + super.width, y, super.borderColor);
		y += 4;

		// Combat level
		y += g.getFontHeight();
		g.drawString("Combat: " + world.getStats().getCombatLevel(), x + 12, y, Color.WHITE, true);

		// Skill total
		y += g.getFontHeight();
		g.drawString("Skill total: " + world.getStats().getSkillTotal(), x + 12, y, Color.WHITE, true);

		y += 8;
		g.drawLine(x, y, x + super.width, y, super.borderColor);
		y += 4;

		if (activeStat == null) {
			y += g.getFontHeight();
			g.drawString("Hover for details.", x + 12, y, Color.WHITE, true);
		}
		else {
			Stat stat = world.getStats().get(activeStat);

			// Experience
			y += g.getFontHeight();
			g.drawString("Exp: " + formatter.format(stat.getExp()), x + 6, y, Color.WHITE, true);

			// Next level
			y += g.getFontHeight();
			g.drawString("Next at: " + formatter.format(Stat.levelToExp(stat.getLevel() + 1)), x + 6, y, Color.WHITE, true);
		}
	}
} public class Stat extends Observable implements Comparable<Stat>, Serializable {
	private static final long serialVersionUID = 1L;

	public static int expToLevel(long exp) {
		return 1; // TODO: calculate the level from the experience
	}

	public static long levelToExp(int level) {
		// NOTE: first ensure level is within bounds, i.e. if level > MAX_LEVEL: level = MAX_LEVEL
		return 0; // TODO: calculate the experience required for the level
	}

	public enum Type {
		ATTACK("Attack"),
		DEFENSE("Defense"),
		STRENGTH("Strength"),
		HITPOINTS("Hitpoints"),
		MAGIC("Magic"),
		ARCHERY("Archery");

		public final String name;

		private Type(String name) {
			this.name = name;
		}
	};

	protected Type type;
	protected long exp;
	protected transient int level;
	protected int current;

	@SuppressWarnings("unused")
	private Stat() { } // for hibernate

	public Stat(Type type, long exp, int current) {
		this.type = type;
		this.current = current;
		this.setExp(exp);
	}

	public void setType(Stat.Type type) {
		this.type = type;
	}

	public Type getType() {
		return type;
	}

	public void setExp(long exp) {
		this.exp = exp;
		this.level = Stat.expToLevel(exp);

		super.setChanged();
		super.notifyObservers(false);
	}

	public long getExp() {
		return exp;
	}

	public void setCurrent(int current) {
		this.current = current;

		super.setChanged();
		super.notifyObservers(false);
	}

	public int getCurrent() {
		return current;
	}

	public int getLevel() {
		return level;
	}

	public boolean incExp(long exp) {
		int level = this.level;
		this.setExp(this.exp + exp);

		if (this.level != level) {
			super.setChanged();
			super.notifyObservers(true);
			return true;
		}

		return false;
	}

	@Override
	public int hashCode() {
		return type.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Stat))
			return false;

		Stat s = (Stat) o;
		return exp == s.exp && current == s.current;
	}

	@Override
	public int compareTo(Stat s) {
		// compares based on the current level, not the experience!
		return current - s.current;
	}

	@Override
	public String toString() {
		return current + "/" + level;
	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		this.setExp(exp); // Re-set the experience, this will calculate the level for us
	}
} public class Keyboard {

	public static final int KEY_UP = 200;
	public static final int KEY_LEFT = 203;
	public static final int KEY_RIGHT = 205;
	public static final int KEY_DOWN = 208;

	public static final int KEY_F1 = 59;
	public static final int KEY_F2 = 60;
	public static final int KEY_F3 = 61;
	public static final int KEY_F4 = 62;
	public static final int KEY_F5 = 63;
	public static final int KEY_F6 = 64;
	public static final int KEY_F7 = 65;
	public static final int KEY_F8 = 66;
	public static final int KEY_F9 = 67;
	public static final int KEY_F10 = 68;
	public static final int KEY_F11 = 87;
	public static final int KEY_F12 = 88;
	public static final int KEY_F13 = 100;
	public static final int KEY_F14 = 101;
	public static final int KEY_F15 = 102;

	public Keyboard() {
		try {
			org.lwjgl.input.Keyboard.enableRepeatEvents(true);
			org.lwjgl.input.Keyboard.create();
		}
		catch (LWJGLException e) {
			// fatal error
			throw new RuntimeException("Error setting up keyboard: " + e);
		}
	}

	public boolean isKeyDown(int keyCode) {
		return org.lwjgl.input.Keyboard.isKeyDown(keyCode);
	}

	public void update(KeyListener listener) {
		while (org.lwjgl.input.Keyboard.next()) {
			// If it wasn't a press, ignore it
			if (!org.lwjgl.input.Keyboard.getEventKeyState())
				continue;

			listener.keyPressed(org.lwjgl.input.Keyboard.getEventKey(), org.lwjgl.input.Keyboard.getEventCharacter());
		}
	}
} public class Graphics2D {
	protected final TextRenderer textRenderer;
	protected final Graphics graphics;
	protected final DisplayMode mode;
	protected final FloatBuffer projMatrix;

	protected Graphics2D(Graphics graphics, DisplayMode mode) {
		this.graphics = graphics;
		this.mode = mode;

		textRenderer = new TextRenderer(graphics);

		// Set up our 2D mode
		GL11.glViewport(graphics.viewport.get(0), graphics.viewport.get(1), graphics.viewport.get(2), graphics.viewport.get(3));
		GL11.glMatrixMode(GL11.GL_PROJECTION);
		GL11.glLoadIdentity();
		GLU.gluOrtho2D(0, mode.getWidth(), mode.getHeight(), 0);
		GL11.glMatrixMode(GL11.GL_MODELVIEW);

		projMatrix = BufferUtils.createFloatBuffer(16);
		GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projMatrix);
	}

	public Graphics getGraphics() {
		return graphics;
	}

	public void begin() {
		GL11.glPushMatrix();
		graphics.setMode(Graphics.Mode.G2D);

		// Disable lighting effects
		GL11.glDisable(GL11.GL_LIGHTING);

		// Disable fog
		GL11.glDisable(GL11.GL_FOG);

		// Disable depth testing
		GL11.glDisable(GL11.GL_DEPTH_TEST);

		// Disable anti-aliasing
		GL11.glDisable(GL11.GL_LINE_SMOOTH);

		// Disable culling
		GL11.glDisable(GL11.GL_CULL_FACE);
	}

	public void end() {
		GL11.glPopMatrix();
	}

	public int getFontWidth(String message) {
		return textRenderer.getWidth(message);
	}

	public int getFontHeight() {
		return textRenderer.getHeight();
	}

	protected void setColor(Color c) {
		GL11.glColor4f(c.getRed() / 255f, c.getGreen() / 255f, c.getBlue() / 255f, c.getAlpha() / 255f);
	}

	public void drawRect(int x, int y, int width, int height, Color c) {
		this.setColor(c);
		GL11.glBegin(GL11.GL_LINE_LOOP);
		{
			GL11.glVertex2i(x, y);
			GL11.glVertex2i(x + width, y);
			GL11.glVertex2i(x + width, y + height);
			GL11.glVertex2i(x, y + height);
		}
		GL11.glEnd();
		this.setColor(Color.WHITE);
	}

	public void fillRect(int x, int y, int width, int height, Color c) {
		this.setColor(c);
		GL11.glBegin(GL11.GL_QUADS);
		{
			GL11.glVertex2i(x, y);
			GL11.glVertex2i(x + width, y);
			GL11.glVertex2i(x + width, y + height);
			GL11.glVertex2i(x, y + height);
		}
		GL11.glEnd();
		this.setColor(Color.WHITE);
	}

	public void drawLine(int x1, int y1, int x2, int y2, Color c) {
		this.setColor(c);
		GL11.glBegin(GL11.GL_LINES);
		{
			GL11.glVertex2i(x1, y1);
			GL11.glVertex2i(x2, y2);
		}
		GL11.glEnd();
		this.setColor(Color.WHITE);
	}

	public void drawString(String str, int x, int y, Color c) {
		this.drawString(str, x, y, c, false);
	}

	public void drawString(String str, int x, int y, Color c, boolean shadow) {
		textRenderer.begin();
		{
			if (shadow) {
				this.setColor(Color.BLACK);
				textRenderer.drawString(str, x + 1, y + 1);
			}

			this.setColor(c);
			textRenderer.drawString(str, x, y);
			this.setColor(Color.WHITE);
		}
		textRenderer.end();
	}

	public void drawSprite(Sprite sprite, int x, int y, int width, int height) {
		// Bind the texture
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, sprite.textureID);

		GL11.glBegin(GL11.GL_QUADS);
		{
			GL11.glTexCoord2f(0, 0);
			GL11.glVertex2i(x, y);

			GL11.glTexCoord2f(0, 1);
			GL11.glVertex2i(x, y + height);

			GL11.glTexCoord2f(1, 1);
			GL11.glVertex2i(x + width, y + height);

			GL11.glTexCoord2f(1, 0);
			GL11.glVertex2i(x + width, y);
		}
		GL11.glEnd();

		// Unbind the texture
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
	}
} public class Icon extends Widget {

	protected final Sprite sprite;

	public Icon(Sprite sprite, int width, int height) {
		super (width, height, true);

		this.sprite = sprite;
	}

	@Override
	public boolean mouseClicked(int x, int y, boolean left) {
		ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "mouseClicked");
		super.processActionEvent(e);

		return true;
	}

	@Override
	public void keyPressed(int keyCode, char keyChar) {
		switch (keyCode) {
		case Keyboard.KEY_RETURN:
			ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "keyPressed");
			super.processActionEvent(e);
			break;
		}
	}

	@Override
	protected void display(Graphics2D g, int x, int y) {
		if (super.backgroundColor != null)
			g.fillRect(x, y, super.width, super.height, super.backgroundColor);

		g.drawSprite(sprite, x, y, super.width, super.height);

		if (super.borderColor != null)
			g.drawRect(x, y, super.width, super.height, super.borderColor);
	}
} public class ProgressBar extends Widget {

	protected int value;
	protected Color color;


	public ProgressBar(int width, int height) {
		super(width, height, false);

		// Default to not started
		color = new Color(0f, 0.5f, 0f);
		value = 0;
	}

	public int getValue() {
		return value;
	}

	public Color getColor() {
		return color;
	}

	@Override
	protected void display(Graphics2D g, int x, int y) {
		if (super.backgroundColor != null)
			g.fillRect(x, y, width, height, super.backgroundColor);

		int offset = (height / 100) * (100 - this.getValue());
		g.fillRect(x, y + offset, width, height - offset, this.getColor());

		if (super.borderColor != null)
			g.drawRect(x, y, width, height, super.borderColor);
	}
} public class TextField extends Widget {

	protected final StringBuilder value;
	protected boolean shadow;

	public TextField(int width, int height) {
		this (width, height, false);
	}

	public TextField(int width, int height, boolean shadow) {
		super (width, height, true);

		this.shadow = shadow;

		value = new StringBuilder();
	}

	public void clear() {
		value.setLength(0);
	}

	public String getText() {
		return value.toString();
	}

	public String getVisibleText() {
		String text = this.getText();
		if (super.hasFocus())
			text += '*';

		return text;
	}

	public void setText(String value) {
		this.clear();
		this.value.append(value);
	}

	@Override
	public void keyPressed(int keyCode, char keyChar) {
		// Handle control characters
		if (Character.isISOControl(keyChar)) {
			switch (keyCode) {
			case Keyboard.KEY_BACK:
				if (value.length() > 0)
					value.setLength(value.length() - 1);
				break;

			case Keyboard.KEY_RETURN:
				ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "keyPressed");
				super.processActionEvent(e);
				break;
			}

			return;
		}

		// Filter out invalid characters
		Character.UnicodeBlock block = Character.UnicodeBlock.of(keyChar);
		if (block == null || block == Character.UnicodeBlock.SPECIALS)
			return;

		value.append(keyChar);
	}

	@Override
	protected void display(Graphics2D g, int x, int y) {
		if (super.backgroundColor != null)
			g.fillRect(x, y, width, height, super.backgroundColor);

		g.drawString(this.getVisibleText(), x + 4, y + ((super.height - g.getFontHeight()) / 2) + g.getFontHeight(), super.textColor, shadow);

		if (super.borderColor != null)
			g.drawRect(x, y, width, height, super.borderColor);
	}
} public abstract class Widget {
	private transient ActionListener actionListener;

	public final int width;
	public final int height;
	public final boolean focusable;

	protected Color backgroundColor;
	protected Color borderColor;
	protected Color textColor;
	protected boolean highlightFocus;

	protected boolean focused;

	public Widget(int width, int height, boolean focusable) {
		this.width = width;
		this.height = height;
		this.focusable = focusable;

		backgroundColor = Color.WHITE;
		borderColor = Color.BLACK;
		textColor = Color.BLACK;
		highlightFocus = true;

		focused = false;
	}
	
	public boolean isFocusable() {
		return focusable;
	}

	public void setTextColor(Color textColor) {
		this.textColor = textColor;
	}

	public void setBackgroundColor(Color backgroundColor) {
		this.backgroundColor = backgroundColor;
	}

	public void setBorderColor(Color borderColor) {
		this.borderColor = borderColor;
	}

	public void setHighlightFocus(boolean highlightFocus) {
		this.highlightFocus = highlightFocus;
	}

	public boolean hasFocus() {
		return focused;
	}

	public void setFocus(boolean focused) {
		this.focused = focused;
	}

	public void addActionListener(ActionListener l) {
		if (l == null)
			return;

		actionListener = AWTEventMulticaster.add(actionListener, l);
	}

	public void removeActionListener(ActionListener l) {
		if (l == null)
			return;

		actionListener = AWTEventMulticaster.remove(actionListener, l);
	}

	protected void processActionEvent(ActionEvent e) {
		if (actionListener != null)
			actionListener.actionPerformed(e);
	}

	public boolean mouseClicked(int x, int y, boolean left) {
		return false;
	}

	public void keyPressed(int keyCode, char keyChar) { }

	protected abstract void display(Graphics2D g, int x, int y);

	public void displayWidget(Graphics2D g, int x, int y) {
		this.display(g, x, y);

		if (this.highlightFocus && this.focusable && this.focused)
			g.drawRect(x - 1, y - 1, width + 2, height + 2, Color.YELLOW);
	}

} public class Window {

	private static class WindowWidget {
		public final Widget widget;
		public final int x;
		public final int y;

		public WindowWidget(Widget widget, int x, int y) {
			this.widget = widget;
			this.x = x;
			this.y = y;
		}
	}

	protected final List<WindowWidget> widgets;
	protected final boolean handleFocus;
	private int focus;

	public Window() {
		this (true);
	}

	public Window(boolean handleFocus) {
		this.handleFocus = handleFocus;

		widgets = new ArrayList<WindowWidget>();
		focus = -1;
	}

	public void add(Widget widget, int x, int y) {
		WindowWidget container = new WindowWidget(widget, x, y);
		widgets.add(container);
	}

	public void display(Graphics2D g, int x, int y) {
		for (WindowWidget container : widgets)
			container.widget.displayWidget(g, x + container.x, y + container.y);
	}

	public void display(Graphics2D g) {
		this.display(g, 0 , 0);
	}

	public boolean mouseClicked(int x, int y, boolean left) {
		if (widgets.isEmpty())
			return false;

		boolean hit = false;
		for (int i = 0;i < widgets.size();i++) {
			WindowWidget container = widgets.get(i);
			int xOff = x - container.x;
			int yOff = y - container.y;

			if (xOff >= 0 && xOff < container.widget.width && yOff >= 0 && yOff < container.widget.height) {
				hit |= container.widget.mouseClicked(xOff, yOff, left);
				if (container.widget.isFocusable())
					this.setFocus(i);
			}
		}

		return hit;
	}

	public void keyPressed(int keyCode, char keyChar) {
		if (widgets.isEmpty())
			return;

		switch (keyCode) {
		case Keyboard.KEY_TAB:
			this.focusNext();
			break;
		default:
			if (focus > -1) {
				Widget widget = widgets.get(focus).widget;
				widget.keyPressed(keyCode, keyChar);
			}
			break;
		}
	}

	public boolean setFocus(int focus) {
		if (!handleFocus || widgets.isEmpty())
			return false;
		
		// Remove focus from the existing widget
		if (this.focus > -1) {
			Widget widget = widgets.get(this.focus).widget;
			if (widget.hasFocus())
				widget.setFocus(false);
		}

		this.focus = focus;

		// Add focus to the new widget
		if (this.focus > -1) {
			Widget widget = widgets.get(this.focus).widget;
			if (widget.isFocusable()) {
				// Focus on the new widget and return success
				widget.setFocus(true);
				return true;
			}
		}
		
		return false;
	}

	public void focusNext() {
		if (!handleFocus)
			return;

		if (widgets.isEmpty())
			this.setFocus(-1);
		else {
			// This will loop infinitely if we call focusNext() and no widgets allow focus!
			while (!this.setFocus((focus + 1) % widgets.size()));
		}
	}
}
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import com.game.client.Client;
import com.game.client.WorldManager;
import com.game.client.ui.menu.Friends;
import com.game.client.ui.menu.Inventory;
import com.game.client.ui.menu.Magic;
import com.game.client.ui.menu.MiniMap;
import com.game.client.ui.menu.Options;
import com.game.client.ui.menu.PlayerStats;
import com.game.common.model.Stat;
import com.game.graphics.input.Keyboard;
import com.game.graphics.renderer.Graphics2D;
import com.game.graphics.widget.Icon;
import com.game.graphics.widget.ProgressBar;
import com.game.graphics.widget.TextField;
import com.game.graphics.widget.Widget;
import com.game.graphics.widget.Window;
package com.game.client.ui;



public class HUD extends Window implements ActionListener {
	private static class MenuIcon {
		public final Icon icon;
		public final Menu menu;

		public MenuIcon(Icon icon, Menu menu) {
			this.icon = icon;
			this.menu = menu;
		}
	};

	private static class FilterIcon {
		public final Icon icon;
		public final ChatMessages.Type type;

		public FilterIcon(Icon icon, ChatMessages.Type type) {
			this.icon = icon;
			this.type = type;
		}
	}

	private static final Color BACKGROUND_COLOR = Color.DARK_GRAY;
	private static final Color BACKGROUND_INPUT_OVERLAY = new Color(0, 0, 0, 0.8f);
	private static final Color BORDER_COLOR = Color.BLACK;
	private static final int SIZE = 32;

	protected final Client client;
	protected final WorldManager world;
	protected final ChatMessages chatMessages;
	protected final TextField chatField;

	protected final ProgressBar healthBar;

	protected final MenuIcon[] menus;
	protected MenuIcon activeMenu;

	protected final FilterIcon[] filters;
	protected FilterIcon activeFilter;

	protected TextField inputBox;
	protected boolean showFPS;

	public HUD(Client client) {
		super (false);

		int x, y;

		this.client = client;
		world = client.getWorldManager();

		menus = new MenuIcon[6];

		// settings
		menus[0] = new MenuIcon(
			new Icon(client.getGraphics().loadSprite("icons/settings.png"), SIZE, SIZE),
			new Options(world)
		);

		// stats
		menus[1] = new MenuIcon(
			new Icon(client.getGraphics().loadSprite("icons/stats.png"), SIZE, SIZE),
			new PlayerStats(world, client.getGraphics(), client.getMouse())
		);

		// map
		menus[2] = new MenuIcon(
			new Icon(client.getGraphics().loadSprite("icons/map.png"), SIZE, SIZE),
			new MiniMap(world, client.getGraphics())