/**
 * <p>
 * Copyright © 2009-2010, Bruce-Robert Pocock
 * </p>
 * <p>
 * This program 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>
 * <p>
 * This program 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>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * </p>
 * 
 * @author brpocock
 */

package org.starhope.util;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.Map.Entry;

import org.json.JSONObject;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.game.AbstractRoom;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.types.CanProcessCommands;

/**
 * Miscellaneous utility methods that might be useful elsewhere
 * 
 * @author brpocock
 */
public class LibMisc {
	/**
	 * An extension class can be registered to supplement or replace
	 * methods in the basic built-in command interpreters. This is where
	 * they're stored.
	 */
	private static final HashMap <Class <?>, Class <?>> extensionClasses = new HashMap <Class <?>, Class <?>> ();

	/**
	 * Maximum number of times to clear the socket input buffer.
	 */
	static int MAX_SHUTDOWN_TRIES = 20;

	/**
	 * TODO: document this field (brpocock, Nov 19, 2009) messages
	 * (LibMisc)
	 */
	private static Properties messages;

	/**
	 * TODO: document this method (brpocock, Nov 24, 2009)
	 * 
	 * @param thing Who are we?
	 * @param when When are we?
	 * @return true if we've arrived on station
	 */
	public static boolean areWeThereYet (final AbstractUser thing,
			final long when) {
		return LibMisc.timeToTarget (thing, when) > 0 ? false : true;
	}

	/**
	 * <p>
	 * Execute a command with a certain method signature from klass,
	 * passing it the JSON parameters as well as the environment
	 * (thread, zone, and user) to run with.
	 * </p>
	 * <p>
	 * The class klass is searched for a method with the name
	 * "do_COMMAND", which must take a Zone, JSONObject, User, and
	 * Integer (as a room number in which the user is standing) as input
	 * parameters. If no such method is found, the extension class (if
	 * any) defined for klass using {@link #loadExtension(Class)} will
	 * also be searched, before reporting an error back to the command
	 * thread.
	 * </p>
	 * 
	 * @param cmd The command's string name
	 * @param jso JSON parameters to that command
	 * @param commandThread The command processor thread
	 * @param user The user initiating the command
	 * @param klass The command-processor class in which to search for
	 *            the command
	 */
	public static void commandJSON (final String cmd,
			final JSONObject jso,
			final CanProcessCommands commandThread,
			final AbstractUser user, final Class <?> klass) {
		final Class <?> extension = LibMisc.loadExtension (klass);

		Method commandProcessor = null;
		try {
			commandProcessor = klass.getMethod ("do_" + cmd,
					JSONObject.class, AbstractUser.class,
					AbstractRoom.class);
		} catch (final SecurityException e) {
			commandThread.sendError_RAW (cmd, e.toString ());
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final NoSuchMethodException e) {
			try {
				commandProcessor = extension.getMethod ("do_" + cmd,
						JSONObject.class, AbstractUser.class,
						AbstractRoom.class);
			} catch (final SecurityException e1) {
				commandThread.sendError_RAW (cmd, e.toString ());
				AppiusClaudiusCaecus.reportBug (e);
			} catch (final NoSuchMethodException e1) {
				commandThread.sendError_RAW ("*", "No such method");
			}
		}
		try {
			if (null != commandProcessor) {
				commandThread.setBusyState (true);
				final JSONObject ret = (JSONObject) commandProcessor
				.invoke (null, jso, user, (null == user ? null
						: user.getRoom ()));
				commandThread.setLastInputTime (System
						.currentTimeMillis ());
				commandThread.setBusyState (false);
				if (null != ret) {
					commandThread.sendResponse (ret);
				}
			}
		} catch (final IllegalArgumentException e) {
			AppiusClaudiusCaecus.reportBug (e);
			commandThread.sendError_RAW (cmd, e.toString ());
		} catch (final IllegalAccessException e) {
			AppiusClaudiusCaecus.reportBug (e);
			commandThread.sendError_RAW (cmd, e.toString ());
		} catch (final InvocationTargetException e) {
			final StringBuilder report = new StringBuilder ();
			if (null != user) {
				report.append ("user: “");
				report.append (user.getAvatarLabel ());
				report.append ("” #");
				report.append (user.getUserID ());
			}
			if (null != user && -1 != user.getRoomNumber ()) {
				report.append (" room “");
				report.append (user.getRoom ().getMoniker ());
				report.append ("” #");
				report.append (user.getRoomNumber ());
				report.append (" in zone “");
				report.append (user.getZone ().getName ());
				report.append ("” ");
			}
			report.append ('\n');
			report.append (jso.toString ());
			report.append ('\n');
			report.append ('\n');
			report.append (AppiusClaudiusCaecus.stringify (e
					.getCause ()));
			AppiusClaudiusCaecus.bugDuplex (
					"Exception while handling JSON command " + cmd,
					report.toString ());
			commandThread
			.sendError_RAW (cmd, e.getCause ().toString ());
		} catch (final Exception e) {
			AppiusClaudiusCaecus
			.reportBug (
					"Generic (unhandled) exception inside request handler",
					e);
			commandThread.sendError_RAW ("*" + cmd, e.toString ());
		}
	}

	/**
	 * TODO: document this method (brpocock, Nov 24, 2009)
	 * 
	 * @param from one thing
	 * @param to another thing
	 * @param when when is it
	 * @return the distance between the two objects
	 */
	public static double distance (final AbstractUser from,
			final AbstractUser to, final long when) {
		LibMisc.timeToTarget (from, when);
		LibMisc.timeToTarget (to, when);
		final double dX = from.getX () - to.getX ();
		final double dY = from.getY () - to.getY ();
		return Math.sqrt (dX * dX + dY * dY);
	}

	/**
	 * TODO: document this method (brpocock, Nov 24, 2009)
	 * 
	 * @param x1 start x abcessa
	 * @param y1 start y ordinate
	 * @param x2 end x abcessa
	 * @param y2 end y ordinate
	 * @return distance
	 */
	private static double distance (final double x1, final double y1,
			final double x2, final double y2) {
		final double dX = x2 - x1;
		final double dY = y2 - y1;
		return Math.sqrt (dX * dX + dY * dY);
	}

	/**
	 * TODO: document this method (brpocock, Jan 18, 2010)
	 * 
	 * @param targetDate a date in the future
	 * @return a human-legible expression describing the time in the
	 *         future
	 */
	public static String formatFutureDate (final Date targetDate) {
		return LibMisc.formatFutureDate (targetDate, "en", "US");
	}

	/**
	 * TODO: document this method (brpocock, Jan 18, 2010)
	 * 
	 * @param targetDate a date in the future
	 * @param language the user's language
	 * @param dialect the user's dialect
	 * @return a human-legible expression describing the time in the
	 *         future
	 */
	public static String formatFutureDate (final Date targetDate,
			final String language, final String dialect) {
		return LibMisc.formatFutureDate_English (targetDate);
	}

	/**
	 * @param targetDate a date in the future
	 * @return a human-legible expression describing the time in the
	 *         future
	 */
	private static String formatFutureDate_English (
			final Date targetDate) {
		final Date now = new Date ();
		if (targetDate.compareTo (now) < 0) return "the past";
		if (targetDate.compareTo (now) == 0) return "right now";

		final long diffSec = targetDate.getTime () - now.getTime ();

		if (diffSec <= 0) return "already";

		if (diffSec == 1) return "a second from now";

		if (diffSec < 6) return "a few seconds from now";

		if (diffSec < 60) return "" + diffSec + " seconds from now";

		if (diffSec < 180) {
			final long remSec = diffSec - 60;
			return "a minute and " + diffSec + " second"
			+ (remSec > 1 ? "s" : "") + " from now";
		}

		if (diffSec < 60 * 90)
			return "" + (int) (diffSec / 60) + " minutes from now";

		if (diffSec < 60 * 60 * 36) {
			final long hours = diffSec / (60 * 60);
			final long minutes = diffSec % (60 * 60) / (60 * 15) * 15;
			return "about " + hours + " hours and " + minutes
			+ " minutes from now";
		}

		final long days = diffSec / (60 * 60 * 24);
		return "about " + days + " days from now";
	}

	/**
	 * Create an user-visible string using metric figures accurate to 1
	 * decimal place, expressing an amount of memory in KiB, MiB, &c. to
	 * one decimal place. For example, “12.1 KiB” or “4.2 GiB”.
	 * 
	 * @param numBytes a number of bytes (size_t)
	 * @return a human-legible string
	 */
	public static String formatMemory (final long numBytes) {
		if (numBytes > 1024L * 1024 * 1024 * 10240)
			return String.format ("%.1f TiB", (double) numBytes
					/ (1024L * 1024 * 1024 * 1024));
		if (numBytes > 1024 * 1024 * 1024)
			return String.format ("%.1f GiB", (double) numBytes
					/ (1024 * 1024 * 1024));
		if (numBytes > 1024 * 10240)
			return String.format ("%.1f MiB", (double) numBytes
					/ (1024 * 1024));
		if (numBytes > 10240)
			return String.format ("%.1f KiB", (numBytes / 1024.0));
		return String.format ("%d bytes", numBytes);
	}

	/**
	 * Generate something that resembles an IP address, but is clearly
	 * not a valid host address. Nonetheless, this address will contain
	 * valid byte values. Some byte in the address will be a 255 or the
	 * address will begin or end with a zero byte. Since this method
	 * uses the object's hash code, it will be consistent for an object.
	 * 
	 * @param o The object for which to generate a fake IP address
	 * @return a string in dotted-quad form
	 */
	public static String genFakeIP (final Object o) {
		final long hash = o.hashCode ();
		final byte eh = (byte)((hash & 0xff000000) >> 030);
		final byte el = (byte)((hash & 0x00ff0000) >> 020);
		final byte ah = (byte)((hash & 0x0000ff00) >> 010);
		final byte al = (byte)((hash & 0x000000ff) >> 000);
		if (-128 == eh || 0x00 == eh || -128 == el || 0x00 == el
				|| -128 == ah || 0x00 == ah || -128 == al || 0x00 == al)
			return String.valueOf (eh) + "." + el + "." + ah + "." + al;
		final byte crunch = (byte) (eh ^ el ^ ah ^ al);
		final byte chn = (byte)((crunch & 0xf0) >> 4);
		final byte chl = (byte)(crunch & 0x0f);
		final byte choose = (byte)((chn ^ chl) & 0x03);
		final boolean toggle = (byte)((chn ^ chl) & 0x08) == 1 ;
		if (toggle) {
			switch (choose) {
			case 0:
				return "255." + el + "." + ah + "." + al;
			case 1:
				return String.valueOf(eh) + ".255." + ah + "." + al;
			case 2:
				return String.valueOf(eh) + "." + el + ".255." + al;
			case 3:
				return String.valueOf(eh) + "." + el + "." + ah + ".255";
			default:
				throw new Error ("Math stopped working.");
			}
		} {
			switch (choose) {
			case 0:
			case 2:
				return "0." + el + "." + ah + "." + al;
			case 1:
			case 3:
				return String.valueOf(eh) + "." + el + "." + ah + ".0";
			default:
				throw new Error ("Math stopped working.");
			}
		}
	}

	/**
	 * @param string WRITEME
	 * @return WRITEME
	 */
	public static String getText (final String string) {
		return LibMisc.getText (string, "en", "US");
	}

	/**
	 * @param string WRITEME
	 * @param language WRITEME
	 * @param dialect WRITEME
	 * @return WRITEME
	 */
	public static String getText (final String string,
			final String language, final String dialect) {
		if ( ! (language.equals ("en") && dialect.equals ("US")))
			return "[" + string + "@" + language + "_" + dialect + "]";
		if (null == LibMisc.messages) {
			LibMisc.initMessages ();
		}
		return LibMisc.messages.getProperty (string, "(MESSAGE: "
				+ string + ")");
	}

	/**
	 * Look for a string key in the message catalogue. If a given key does exist, return that message. If not, fall back
	 * to the provided default text.
	 *
	 * @param key The key name which should exist in the message catalogue.
	 * @param fallback  The fallback text to be used if that message does not exist.
	 * @return Either the message from the catalogue, or the fallback message if one is unavailable.
	 */
	public static String getTextOrDefault (final String key, final String fallback) {
		if (LibMisc.hasText (key)) return LibMisc.getText (key);
		return fallback;
	}

	/**
	 * TODO: document this method (brpocock, Jan 5, 2010)
	 * 
	 * @param string WRITEME
	 * @return WRITEME
	 */
	public static boolean hasText (final String string) {
		return LibMisc.hasText (string, "en", "US");
	}

	/**
	 * TODO: document this method (brpocock, Jan 5, 2010)
	 * 
	 * @param string WRITEME
	 * @param language WRITEME
	 * @param dialect WRITEME
	 * @return WRITEME
	 */
	public static boolean hasText (final String string,
			final String language, final String dialect) {
		if ( ! (language.equals ("en") && dialect.equals ("US")))
			return false;
		if (null == LibMisc.messages) {
			LibMisc.initMessages ();
		}
		return LibMisc.messages.containsKey (string);
	}

	/**
	 * Convert an array of bytes into a string of ASCII hexadecimal
	 * nybbles.
	 * 
	 * @param input The bytes to be converted to hex
	 * @return the hex equivalent
	 */
	public static String hexify (final byte [] input) {
		final StringBuilder sha1hex = new StringBuilder (
				input.length * 2);
		for (final byte element : input) {
			sha1hex.append (Integer.toString (
					(element & 0xff) + 0x100, 16).substring (1));
		}
		return sha1hex.toString ();
	}

	/**
	 * Initialize the configured message catalog. Does not currently
	 * cope with languages and dialects properly. (TODO: have multiple
	 * properties files, for various languages and dialects, and replace
	 * the single "messages" attribute with a Map or similar.)
	 */
	private static void initMessages () {
		FileReader filer = null;
		try {
			LibMisc.messages = new Properties ();
			filer = new FileReader (AppiusConfig.getConfigOrDefault (
					"org.starhope.appius.mb.messageFile",
			"/etc/appius/messages/mb.properties"));
			LibMisc.messages.load (filer);
		} catch (final FileNotFoundException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} catch (final IOException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			try {
				if (null != filer) {
					filer.close ();
					filer = null;
				}
			} catch (final IOException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
		}
	}

	/**
	 * TODO: document this method (brpocock, Jan 12, 2010)
	 * 
	 * @param words WRITEME
	 * @param language WRITEME
	 * @param dialect WRITEME
	 * @return WRITEME
	 */
	public static String listToDisplay (
			final Collection <? extends Object> words,
			final String language, final String dialect) {
		final List <String> wordList = new LinkedList <String> ();
		for (final Object o : words) {
			wordList.add (o.toString ());
		}
		return LibMisc.listToDisplay (wordList, language, dialect);
	}

	/**
	 * <p>
	 * Given a list of strings, combine then into a string for display
	 * purposes.
	 * </p>
	 * <p>
	 * For English, the list will obey the traditional grammatical usage
	 * of commas: List elements are joined with commas, except that the
	 * conjunction (in our case, always “and”) occurs penultimate, and
	 * two or three element lists do not use commas.
	 * </p>
	 * <p>
	 * For Spanish, works essentially the same way.
	 * </p>
	 * <p>
	 * For other languages, we just join the words with commas and omit
	 * the conjunction
	 * </p>
	 * 
	 * @param words A list of words.
	 * @param language The user's display language
	 * @param dialect The user's sublanguage dialect
	 * @return The list formatted into a string.
	 */
	public static String listToDisplay (final List <String> words,
			final String language, final String dialect) {
		if (words.size () == 1) return words.get (0);
		if (language.equals ("en"))
			return LibMisc.listToDisplay_English (words);
		else if (language.equals ("es"))
			return LibMisc.listToDisplay_Español (words);
		else if (language.equals ("pf"))
			return LibMisc.listToDisplay_فرسئ (words);
		final StringBuilder result = new StringBuilder ();
		for (int i = 0; i < words.size (); ++i) {
			result.append (words.get (i));
			if (i < words.size ()) {
				result.append (", ");
			}
		}
		return result.toString ();

	}

	/**
	 * @param set WRITEME
	 * @param language WRITEME
	 * @param dialect WRITEME
	 * @return WRITEME
	 */
	public static String listToDisplay (final Object [] set,
			final String language, final String dialect) {
		final LinkedList <String> stuff = new LinkedList <String> ();
		for (final Object o : set) {
			stuff.add (o.toString ());
		}
		return LibMisc.listToDisplay (stuff, language, dialect);
	}

	/**
	 *Internal helper method for
	 * {@link #listToDisplay(List, String, String)} for English.
	 * 
	 * @param words word list
	 * @return list formatted for display in English
	 */
	private static String listToDisplay_English (
			final List <String> words) {
		if (words.size () == 2)
			return words.get (0) + " and " + words.get (1);
		if (words.size () == 3)
			return words.get (0) + ", " + words.get (1) + " and "
			+ words.get (2);
		final StringBuilder result = new StringBuilder ();
		for (int i = 0; i < words.size (); ++i) {
			result.append (words.get (i));
			if (i < words.size ()) {
				result.append (", ");
			}
			if (i == words.size () - 1) {
				result.append ("and ");
			}
		}
		return result.toString ();
	}

	/**
	 *Internal helper method for
	 * {@link #listToDisplay(List, String, String)} for Spanish.
	 * 
	 * @param words word list
	 * @return list formatted for display in Spanish
	 */
	private static String listToDisplay_Español (
			final List <String> words) {
		if (words.size () == 2)
			return words.get (0) + " y " + words.get (1);
		if (words.size () == 3)
			return words.get (0) + ", " + words.get (1) + " y "
			+ words.get (2);
		final StringBuilder result = new StringBuilder ();
		for (int i = 0; i < words.size (); ++i) {
			result.append (words.get (i));
			if (i < words.size ()) {
				result.append (", ");
			}
			if (i == words.size () - 1) {
				result.append ("y ");
			}
		}
		return result.toString ();
	}

	/**
	 *Internal helper method for
	 * {@link #listToDisplay(List, String, String)} for Persian.
	 * 
	 * @param words word list
	 * @return list formatted for display in Persian
	 */
	private static String listToDisplay_فرسئ (final List <String> words) {
		if (words.size () == 2)
			return words.get (0) + " وا" + words.get (1);
		if (words.size () == 3)
			return words.get (0) + " وا " + words.get (1) + " وا "
			+ words.get (2);
		final StringBuilder result = new StringBuilder ();
		for (int i = 0; i < words.size (); ++i) {
			result.append (words.get (i));
			if (i < words.size ()) {
				result.append ("، ");
			}
			if (i == words.size () - 1) {
				result.append (" وا ");
			}
		}
		return result.toString ();
	}

	/**
	 * Find a substitute class for a command interpreter class with
	 * local extensions. These are defined by specifying the canonical
	 * class name in the configuration as a parameter to a key of the
	 * form "xtn." plus the canonical class name of the main class to
	 * which the extension applies.
	 * 
	 * @param klass The class for which an extension might exist
	 * @return An extension class, if one exists; else, the same class
	 *         as passed-in
	 */
	public static Class <?> loadExtension (final Class <?> klass) {
		synchronized (LibMisc.extensionClasses) {
			Class <?> extension = LibMisc.extensionClasses.get (klass);
			if (null != extension) return extension;
			try {
				extension = Class
				.forName (AppiusConfig.getConfig ("xtn."
						+ klass.getCanonicalName ()));
			} catch (final ClassNotFoundException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a ClassNotFoundException in loadExtension",
						e);
				return klass;
			} catch (final NotFoundException e) {
				/* Not having an extension class is not per se an error */
				return klass;
			}
			LibMisc.extensionClasses.put (klass, extension);
			return extension;
		}
	}

	/**
	 * TODO: document this method (brpocock, Dec 1, 2009)
	 * 
	 * @param string WRITEME
	 * @return WRITEME
	 */
	public static int makeHashCode (final String string) {
		byte [] sha1digest;
		try {
			final MessageDigest stomach = MessageDigest
			.getInstance ("SHA1");
			stomach.reset ();
			try {
				stomach.update (string.getBytes ("US-ASCII"));
			} catch (final UnsupportedEncodingException e) {
				AppiusClaudiusCaecus.reportBug ("can't go 16 bit", e);
			}
			sha1digest = stomach.digest ();
		} catch (final NoSuchAlgorithmException e) {
			throw AppiusClaudiusCaecus.fatalBug (new Exception (
			"Can't understand how to do SHA1 digests"));
		} catch (final NumberFormatException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (new Exception (
			"Can't do some magic to make the SHA1 digest for hashing a user"));
		}
		int hash = 0;
		for (final byte n : sha1digest) {
			hash += n ^ 0xaa;
		}
		return hash;

	}

	/**
	 * Perform rot-13 on basic alphabetic characters in the font
	 * 
	 * @param msg The string to be rot-13:ed
	 * @return The string after rot-13
	 */
	public static String rot13 (final String msg) {
		final StringBuilder out = new StringBuilder ();

		final char [] chars = new char [msg.length ()];
		msg.getChars (0, msg.length (), chars, 0);

		for (final char ch : chars) {
			if (ch >= 'a' && ch <= 'm' || ch >= 'A' && ch <= 'M') {
				out.append ((char) (ch + 13));
			} else if (ch >= 'n' && ch <= 'z' || ch >= 'N' && ch <= 'Z') {
				out.append ((char) (ch - 13));
			} else {
				out.append (ch);
			}
		}
		return out.toString ();
	}

	/**
	 * 
	 * Set the maximum number of times to clear the socket input buffer.
	 * 
	 * @param mst maximum number of tries to shut down a socket input
	 *            buffer
	 */
	public static void setMaxShutdownTries (final int mst) {
		LibMisc.MAX_SHUTDOWN_TRIES = mst;
	}

	/**
	 * <p>
	 * This method taken from Apache Tomcat:
	 * </p>
	 * <p>
	 * Licensed to the Apache Software Foundation (ASF) under one or
	 * more contributor license agreements. See the NOTICE file
	 * distributed with this work for additional information regarding
	 * copyright ownership. The ASF licenses this file to You under the
	 * Apache License, Version 2.0 (the "License"); you may not use this
	 * file except in compliance with the License. You may obtain a copy
	 * of the License at
	 *</p>
	 *<p>
	 * http://www.apache.org/licenses/LICENSE-2.0
	 *</p>
	 *<p>
	 * Unless required by applicable law or agreed to in writing,
	 * software distributed under the License is distributed on an
	 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
	 * either express or implied. See the License for the specific
	 * language governing permissions and limitations under the License.
	 * </p>
	 * <p>
	 * Shut down the input stream of a connection
	 *</p>
	 * 
	 * @param socket The socket whose input stream is to be shut down
	 * @throws IOException If the input stream cannot be shut down
	 *         despite best efforts
	 */
	public static void shutdownInput (final Socket socket)
	throws IOException {
		try {
			final InputStream is = socket.getInputStream ();
			int available = is.available ();
			int count = 0;

			// XXX on JDK 1.3 just socket.shutdownInput () which
			// was added just to deal with such issues.

			// skip any unread (bogus) bytes
			while (available > 0
					&& count++ < LibMisc.MAX_SHUTDOWN_TRIES) {
				is.skip (available);
				available = is.available ();
			}
		} catch (final NullPointerException npe) {
			// do nothing - we are just cleaning up, this is
			// a workaround for Netscape \n\r in POST - it is supposed
			// to be ignored
		}
	}

	/**
	 * Sort the contents of a hash map based upon comparing the values
	 * of its keys. This implementation takes a hash map tying integers
	 * to integers, and is used for e.g. sorting high scores by players
	 * 
	 * @param stuffToSort The hash table to be sorted
	 * @return the stuff all sorted
	 */
	public static LinkedHashMap <Integer, Integer> sortHashMapByValues (
			final Map <Integer, Integer> stuffToSort) {
		final List <Integer> mapKeys = new ArrayList <Integer> (
				stuffToSort.keySet ());
		final List <Integer> mapValues = new ArrayList <Integer> (
				stuffToSort.values ());
		Collections.sort (mapValues);
		Collections.sort (mapKeys);

		final LinkedHashMap <Integer, Integer> sortedMap = new LinkedHashMap <Integer, Integer> ();

		final Iterator <Integer> valueIt = mapValues.iterator ();
		while (valueIt.hasNext ()) {
			final Integer val = valueIt.next ();
			final Iterator <Integer> keyIt = mapKeys.iterator ();

			while (keyIt.hasNext ()) {
				final Integer key = keyIt.next ();
				final String comp1 = stuffToSort.get (key).toString ();
				final String comp2 = val.toString ();

				if (comp1.equals (comp2)) {
					stuffToSort.remove (key);
					mapKeys.remove (key);
					sortedMap.put (key, val);
					break;
				}

			}

		}
		return sortedMap;
	}

	/**
	 * Convert a map into a string, mostly for debugging purposes.
	 * 
	 * @param map a map object to be stringified
	 * @return a string containing all keys and values in the map
	 */
	public static String stringify (final Map <Object, Object> map) {
		final StringBuilder b = new StringBuilder ();
		for (final Entry <Object, Object> tuple : map.entrySet ()) {
			b.append ("“");
			b.append (tuple.getKey ());
			b.append ("”:\t“");
			b.append (tuple.getValue ());
			b.append ("”\n");
		}
		return b.toString ();
	}

	/**
	 * TODO: document this method (brpocock, Nov 24, 2009)
	 * 
	 * @param thing what is moving
	 * @param when what time is it now
	 * @return how long until it gets there
	 */
	public static long timeToTarget (final AbstractUser thing,
			final long when) {
		double x1 = thing.getX ();
		double y1 = thing.getY ();
		final double x2 = thing.getTargetX ();
		final double y2 = thing.getTargetY ();
		final long startT = thing.getTravelStart ();
		final double rate = thing.getTravelRate ();

		final long traveledT = when - startT;
		final long tripT = (long) (LibMisc.distance (x1, y1, x2, y2) / rate);

		if (traveledT > tripT) {
			thing.setX (x2);
			thing.setY (y2);
			thing.setStartT (when);
			thing.setTravelRate (0);
			return 0;
		}

		x1 += (x2 - x1) * (tripT - traveledT);
		y1 += (y2 - y1) * (tripT - traveledT);
		thing.setX (x1);
		thing.setY (y1);
		thing.setStartT (when);

		if (x2 - 1 < x1 && x2 + 1 > x1 && y2 - 1 < y1 && y2 + 1 > y1) {
			thing.setX (x2);
			thing.setY (y2);
			thing.setStartT (when);
			thing.setTravelRate (0);
			return 0;
		}

		return tripT - traveledT;
	}

}
