/**
 * <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.appius.game;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.*;
import org.starhope.appius.game.inventory.ClothingItem;
import org.starhope.appius.game.inventory.HomeDecorItem;
import org.starhope.appius.game.inventory.InventoryItem;
import org.starhope.appius.sys.op.FilterResult;
import org.starhope.appius.sys.op.OpCommands;
import org.starhope.appius.types.AbstractZone;
import org.starhope.appius.types.Colour;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.User;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;

import com.tootsville.Store;
import com.tootsville.user.Toot;

/**
 * <h1>Command processor for Appius Game Server (JSON Commands)</h1>
 * <p>
 * This is the command interpreter library for JSON command received
 * from the game client. The server will search for commands in the
 * general library (using Java reflection to examine this class), as
 * well as a configuration-specified local “library” class for
 * extensions specific to a given game.
 * </p>
 * <p>
 * This command processor is scanned for a method name matching the
 * command name specified in the RPC call (JSON call) from the client.
 * These commands can be invoked by any client.
 * </p>
 * <p>
 * Command names must start with <tt>do_</tt>, followed by the
 * (typically javaCamelCased) command verb. The method signature must be
 * exactly <tt> public static void do_</tt> <i>verb</i>
 * <tt> (Zone, JSONObject, User, Integer) </tt>.
 * </p>
 * <p>
 * Security is generally <em>voluntary</em> and must be enforced by
 * individual methods.
 * </p>
 * 
 * @author brpocock
 * 
 */
public class Commands {

	/**
	 * @see #do_setFurniture(JSONObject, AbstractUser, AbstractRoom)
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *         from the JSON objects passed in, or conversely, if we
	 *         can't encode a response into a JSON form
	 * @throws NotFoundException WRITEME
	 */
	public static void do_addFurniture (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException, NotFoundException {
		Commands.do_setFurniture (jso, u, room);
	}

	/**
	 * 
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @param jso { buddy: (name) } or { ignore: (name) } or { buddy:
	 *            (name), confirm: (boolean), sign: (signature) } or
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_addToList (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		if (jso.has ("buddy")) {
			final AbstractUser bud = User.getByLogin (jso
					.getString ("buddy"));
			if (null == bud) return;
			if (jso.has ("confirm")) {
				if (jso.getBoolean ("confirm")) {
					// confirm a signed buddy request
					AppiusClaudiusCaecus.blather ("", u.getZone ()
							.toString (), "",
							"Confirmed buddy:ness. Let's be friends",
							false);
					if (jso.getString ("sign").equals (
							Commands.getBuddySignature (bud, u))) {
						if (u instanceof User) {
							((User) u)
							.blog ("Buddy signature confirmed");
						}
					} else {
						if (u instanceof User) {
							((User) u)
							.blog ("Buddy signature failed; proceeding regardless");
						}
					}
					final AbstractUser self = u;
					final AbstractUser buddy = User.getByLogin (jso
							.getString ("buddy"));

					self.addBuddy (buddy);
					buddy.addBuddy (self);

					buddy.sendUserLists ();
					self.sendUserLists ();
				} else {
					// no op
				}
			} else { // new request
				jso.put ("sign", Commands.getBuddySignature (u, bud));
				jso.remove ("buddy");
				jso.put ("sender", u.getAvatarLabel ());
				bud.acceptSuccessReply ("buddyRequest", jso, bud
						.getRoom ());
			}
		} else {
			u.ignore (User.getByLogin (jso.getString ("ignore")));
			u.acceptSuccessReply ("addToList", jso, room);
		}
	}

	/**
	 * <p>
	 * Response from the first run screen for the user's house
	 * </p>
	 * 
	 * @param jso Data describing the user's lot { lot: lot-ID, house:
	 *            house-ID }
	 * @param u The user buying the lot
	 * @param room The room in which the user is found — for
	 *            communications purposes, at least.
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_createUserHouse (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		if ( ! (u instanceof Toot)) return;
		final Toot user = (Toot) u;
		user.setLot (Integer.parseInt (jso.getString ("lot")));
		user
		.setHouseTypeID (Integer.parseInt (jso
				.getString ("house")));
		user.addRoom (0);
		user.addDefaultFreeItem (36);
		user.addDefaultFreeItem (37);
		user.addDefaultFreeItem (38);
		u.acceptSuccessReply ("createUserHouse", jso, room);
	}

	/**
	 * <p>
	 * JSON object contains "type" = either "clothes" or "pivitz". Does
	 * not affect patterns.
	 * </p>
	 * <p>
	 * Response with total avatar info from "wardrobe"
	 * </p>
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_doff (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		final JSONObject reply = new JSONObject ();
		if (jso.getString ("type").equals ("pivitz")) {
			if (u instanceof Toot) {
				((Toot) u).doffPivitz ();
				reply.put ("type", "pivitz");
				u.acceptSuccessReply ("doff", reply, room);
			}
		} else {
			u.doffClothes ();
			reply.put ("type", "clothes");
			u.acceptSuccessReply ("doff", reply, room);
		}
		u.sendWardrobe ();
		reply.put ("isActive", false);
		Commands.do_getInventoryByType (reply, u, room);
	}

	/**
	 * <p>
	 * JSON object has the item ID to be worn (clothes, patterns,
	 * pivitz) and optionally set the color (for patterns)
	 * </p>
	 * <p>
	 * Response with total avatar info from "wardrobe"
	 * </p>
	 * 
	 * @param jso { item = (itemID) }
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 * @throws DataException for bad colour
	 * @throws NumberFormatException for bad colour numeric parts
	 */
	public static void do_don (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException, NumberFormatException, DataException {
		final InventoryItem item = InventoryItem.getByID (jso
				.getInt ("item"));

		if (null == item) {
			u.acceptErrorReply ("don", "item.notFound", null, room);
			return;
		}

		final ClothingItem clothes = item.asClothing ();
		if (null == clothes) {
			u.acceptErrorReply ("don", "item.notClothing", null, room);
			return;
		}
		if (jso.has ("color") && jso.getString ("color").length () > 0) {
			u.wear (clothes, new Colour (jso.getString ("color")));
		} else {
			u.wear (clothes);
		}

		u.sendWardrobe ();

		final JSONObject blah = new JSONObject ();
		if (item.isPattern ()) {
			// no op
		} else if (item.isPivitz ()) {
			blah.put ("type", "pivitz");
			blah.put ("isActive", false);
			Commands.do_getInventoryByType (blah, u, room);
		} else if (item.isClothingOnly ()) {
			// blah.put ("type", "clothes");
			// do_getInventoryByType (blah, u, room);
		}
	}

	/**
	 * <p>
	 * Echoes back the supplied ActionScript object to the client. This
	 * method exists solely for testing purposes.
	 * </p>
	 * <p>
	 * Sends response containing:
	 * </p>
	 * <ul>
	 * <li>ECHO => Can you hear me now?</li>
	 * <li>status => true</li>
	 * <li>You said: => (the ActionScript object passed in)</li>
	 * </ul>
	 * 
	 * 
	 * @param jso Any JSON object, the contents of which will be
	 *            returned to the caller.
	 * @param u The user calling (to whom the response is sent)
	 * @param room The room in which the user calls us (ignored)
	 */
	public static void do_echo (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room) {
		final JSONObject result = new JSONObject ();
		try {
			result.put ("ECHO", "Can you hear me now?");
			result.put ("status", true);
			result.put ("You said:", jso.get ("z"));
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 14, 2009)
			AppiusClaudiusCaecus.reportBug (e);
		}
		try {
			final AppiusClaudiusCaecus thread = u.getServerThread ();
			if (null != thread) {
				thread.sendResponse (result, room.getID (), u);
			}
		} catch (final UserDeadException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a UserDeadException in do_echo", e);
		}
	}

	/**
	 * TODO: document this method (brpocock, Nov 13, 2009)
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_gameAction (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {

		final int shadeHurts = AppiusConfig.getIntOrDefault (
				"com.tootsville.shade.hurts", 100);
		if (shadeHurts != 0)
			if ("gotShot".equals (jso.getString ("action"))
					&& "shade".equals (jso.getString ("shooter"))
					&& u.getAvatarLabel ().equals (
							jso.getString ("victim"))) {
				try {
					if (u instanceof Toot) {
						((Toot) u).transferPeanuts (BigDecimal.valueOf (
								shadeHurts).negate (), ((Toot) User
										.getByLogin ("Shade")), "shaddowFalls");
					}
				} catch (final AlreadyExistsException e) {
					AppiusClaudiusCaecus
					.reportBug (
							"Caught a AlreadyExistsException when getting molested by Shade: should not occur",
							e);
				}
			}

		final int kaTootelEggMin = AppiusConfig.getIntOrDefault (
				"com.tootsville.kaTootel.eggMin", 10);
		final int kaTootelEggMax = AppiusConfig.getIntOrDefault (
				"com.tootsville.kaTootel.eggMin", 100);
		if (kaTootelEggMin != 0 || kaTootelEggMax != 0)
			if ("gotShot".equals (jso.getString ("action"))
					&& "katootel".equals (jso.getString ("shooter"))
					&& u.getAvatarLabel ().equals (
							jso.getString ("victim"))) {
				try {
					if (u instanceof Toot) {
						((Toot) u).giftPeanuts (
								BigDecimal.valueOf (AppiusConfig
										.getRandomInt (kaTootelEggMin,
												kaTootelEggMax)),
						"givenuts");
					}
				} catch (final AlreadyExistsException e) {
					AppiusClaudiusCaecus
					.reportBug (
							"Caught a AlreadyExistsException when getting molested by Shade: should not occur",
							e);
				}
			}

		for (final RoomListener listener : room.getAllListeners ()) {
			listener.acceptGameAction (u, jso);
		}

	}

	/**
	 * Get avatar data for a list of (other) users.
	 * 
	 * @param jso JSON object, with (ignored) keys tied to values which
	 *            must be the names of users.
	 * @param u The calling user. The calling user's avatar data will
	 *            <em>not</em> be returned.
	 * @param room the (ignored) room in which the method is being
	 *            called
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	@SuppressWarnings ("unchecked")
	public static void do_getAvatars (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		final Iterator <String> keys = jso.keys ();
		final JSONObject list = new JSONObject ();
		while (keys.hasNext ()) {
			final String key = keys.next ();
			try {
				final String fellowsName = jso.getString (key);
				if ( !fellowsName.equals (u.getAvatarLabel ())) {
					final AbstractUser fellow = User
					.getByLogin (fellowsName);
					if (null != fellow) {
						list.put (fellowsName, fellow.getPublicInfo ());
					}
				}
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.reportBug (e);
				// continue on to next key, however.
			}
		}
		final JSONObject result = new JSONObject ();
		try {
			result.put ("avatars", list);
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 14, 2009)
			AppiusClaudiusCaecus.reportBug (e);
		}
		u.acceptSuccessReply ("avatars", result, room);
	}

	/**
	 * 
	 * returns palettes in "extraColors", "baseColors", "patternColors"
	 * in the JSON result object (from: "getColorPalettes")
	 * 
	 * @param jso JSON parameters: ignored
	 * @param u calling user
	 * @param room calling user's room
	 * @throws SQLException if something squirrelly happens
	 * @throws JSONException if something squirrelly happens
	 */
	public static void do_getColorPalettes (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws SQLException, JSONException {
		final JSONObject results = new JSONObject ();
		final JSONObject extraColors = new JSONObject ();
		final JSONObject baseColors = new JSONObject ();
		final JSONObject patternColors = new JSONObject ();

		final Connection conn = AppiusConfig.getDatabaseConnection ();
		PreparedStatement getExtra = null;
		PreparedStatement getBase = null;
		PreparedStatement getPattern = null;
		try {

			int i = 0;

			getExtra = conn
			.prepareStatement ("SELECT name, shade1 AS color FROM avatarHilights");
			if (getExtra.execute ()) {
				final ResultSet extras = getExtra.getResultSet ();
				while (extras.next ()) {
					extraColors.put (String.valueOf (i++ ), extras
							.getInt ("color"));
				}
			}
			getExtra.close ();
			getExtra = null;
		} catch (final SQLException e) {
			throw e;
		} finally {
			if (null != getExtra) {
				try {
					getExtra.close ();
				} catch (final SQLException e) {
					AppiusClaudiusCaecus.reportBug ("In finally block",
							e);
				}
				getExtra = null;
			}
		}
		try {
			int i = 0;

			getBase = conn
			.prepareStatement ("SELECT name, shade2 AS color FROM avatarShades");
			if (getBase.execute ()) {
				final ResultSet bases = getBase.getResultSet ();
				while (bases.next ()) {
					baseColors.put (String.valueOf (i++ ), bases
							.getInt ("color"));
				}
			}
			getBase.close ();
			getBase = null;
		} catch (final SQLException e) {
			throw e;
		} finally {
			if (null != getBase) {
				try {
					getBase.close ();
				} catch (final SQLException e) {
					AppiusClaudiusCaecus.reportBug ("In finally block",
							e);
				}
				getBase = null;
			}
		}
		try {
			int i = 0;

			getPattern = conn
			.prepareStatement ("SELECT name, shade1 AS color FROM avatarColors");
			if (getPattern.execute ()) {
				final ResultSet patterns = getPattern.getResultSet ();
				while (patterns.next ()) {
					patternColors.put (String.valueOf (i++ ), patterns
							.getInt ("color"));
				}
			}
			getPattern.close ();
			getPattern = null;

		} catch (final SQLException e) {
			throw e;
		} finally {
			if (null != getPattern) {
				try {
					getPattern.close ();
				} catch (final SQLException e) {
					AppiusClaudiusCaecus.reportBug ("In finally block",
							e);
				}
				getPattern = null;
			}

		}

		results.put ("extraColors", extraColors);
		results.put ("baseColors", baseColors);
		results.put ("patternColors", patternColors);
		u.acceptSuccessReply ("getColorPalettes", results, room);
	}

	/**
	 * <p>
	 * JSON object has the type of item from the following list of
	 * strings: "clothes" "patterns" "pivitz" "furniture" "structure"
	 * "music"
	 * </p>
	 * <p>
	 * Returns a set of items as inv: { 0: { id: 123, isActive: boolean
	 * }, ... } — furniture with placement data will also have x, y, and
	 * facing vars. Other attributes are "from":"inventory", "type":
	 * matching the type of the question
	 * </p>
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_getInventoryByType (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		final String typeString = jso.getString ("type");
		final JSONObject inv = new JSONObject ();
		int i = 0;
		for (final InventoryItem item : u.getItemsByType (typeString)) {
			if ( !item.isActive ()) {
				inv.put (String.valueOf (i++ ), item.toJSON ());
			}
		}
		final JSONObject response = new JSONObject ();
		response.put ("inv", inv);
		response.put ("type", typeString);
		u
		.acceptSuccessReply ("inventory", response, room);
	}

	/**
	 * Get a list of users in a Zone, or in a Room. This is an
	 * administrative function, only available to staff members.
	 * 
	 * @param jso The JSON data provided by the caller. If this contains
	 *            an attribute of "inRoom" with a room moniker, we'll
	 *            only return the users in that room. Otherwise, all
	 *            users in the Zone will be returned.
	 * @param u The caller's ID. Must have staff privileges.
	 * @param room The room from which the caller is making the
	 *            extension call: ignored.
	 * @throws JSONException if the JSON data can't be processed, in or
	 *             out.
	 * @throws PrivilegeRequiredException if the user doesn't have
	 *             STAFF_LEVEL_STAFF_MEMBER
	 */
	public static void do_getOnlineUsers (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException, PrivilegeRequiredException {

		u.assertStaffLevel (User.STAFF_LEVEL_STAFF_MEMBER);

		final JSONObject response = new JSONObject ();
		final JSONObject userList = new JSONObject ();

		if (jso.has ("inRoom")) {
			final String roomName = jso.getString ("inRoom");
			response.put ("inRoom", roomName);
			for (final AbstractUser user : room.getZone ()
					.getRoomByName (roomName).getAllUsers ()) {
				userList.put (String.valueOf (user.getUserID ()), user
						.getAvatarLabel ());
			}
		} else {
			final JSONObject roomsJSO = new JSONObject ();
			final Iterator <AbstractRoom> roomIter = room.getZone ()
			.getRoomList ().iterator ();
			while (roomIter.hasNext ()) {
				final AbstractRoom thatRoom = roomIter.next ();
				final String roomName = thatRoom.getName ();
				final JSONObject roomObj = new JSONObject ();
				for (final AbstractUser user : room.getZone ()
						.getRoomByName (roomName).getAllUsers ()) {
					roomObj.put (String.valueOf (user.getUserID ()),
							user.getAvatarLabel ());
				}
				roomsJSO.put (roomName, roomObj);
			}
			response.put ("rooms", roomsJSO);
			final Iterator <AbstractUser> users = room.getZone ()
			.getAllUsersInZone ().iterator ();
			while (users.hasNext ()) {
				final String userName = users.next ().getAvatarLabel ();
				userList.put (userName, userName);
			}
		}

		response.put ("userList", userList);

		u.acceptSuccessReply ("getUserList", response, room);
	}

	/**
	 * <p>
	 * Send the server time to the client requesting it (for
	 * synchronization purposes)
	 * </p>
	 * 
	 * <p>
	 * Sends a JSON object with a single property, serverTime, with the
	 * current time in milliseconds (give or take transit time)
	 * </p>
	 * 
	 * @param jso ignored
	 * @param u The user requesting the time
	 * @param room The room in which the user is standing
	 * @throws JSONException If the JSON data can't be written out
	 */
	public static void do_getServerTime (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		final JSONObject time = new JSONObject ();
		time.put ("serverTime", System.currentTimeMillis ());
		final AbstractRoom thatRoom = room;
		if (null != thatRoom) {
			final GameEvent game = thatRoom.getGameEvent ();
			if (null != game) {
				final long gameTime = game.getTimer ();
				if (gameTime > 0) {
					time.put ("gameTime", gameTime);
				}
			}
		}
		u.acceptSuccessReply ("serverTime", time, room);
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 26, 2009)
	 * <p>
	 * Results: totalPeanuts, stores...
	 * </p>
	 * <ul>
	 * <li>"stores"
	 * <ul>
	 * <li>store number
	 * <ul>
	 * <li>"items"
	 * <ul>
	 * <li>sequence index
	 * <ul>
	 * <li>"id": id number</li>
	 * <li>"title": title</li>
	 * <li>"price": price in peanuts</li>
	 * </ul>
	 * </li>
	 * </ul>
	 * </li>
	 * </ul>
	 * </li>
	 * </ul>
	 * </li>
	 * </ul>
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws NumberFormatException WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	@SuppressWarnings ( { "cast", "unchecked" })
	public static void do_getStoreItems (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws NumberFormatException, JSONException {
		final JSONObject result = new JSONObject ();
		final JSONObject stores = new JSONObject ();
		final Iterator <String> keys = (Iterator <String>) jso.keys ();
		while (keys.hasNext ()) {
			final String key = keys.next ();
			final int storeID = jso.getInt (key);
			stores.put (String.valueOf (storeID), Store.getByID (
					storeID).toJSON ());
		}
		if (u instanceof Toot) {
			result.put ("totalPeanuts", ((Toot) u).getPeanuts ()
					.toPlainString ());
		}
		result.put ("stores", stores);
		u.acceptSuccessReply ("getStoreItems", result, room);
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * <p>
	 * { buddyList: { 0: { buddy-notice... } , 1: { buddy-notice... } ,
	 * ... } , ignoreList: { 0: ignoredUserName, 1: ignoredUserName, ...
	 * } }
	 * </p>
	 * 
	 * @param jso no parameters needed
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_getUserLists (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		u.sendUserLists ();
	}

	/**
	 * Get a list of all Zones currently active/visible. (Empty and
	 * retired Zones are culled. See
	 * {@link AbstractZone#getZoneList_JSON} )
	 * 
	 * @param jso Ignored
	 * @param u The user requesting the data.
	 * @param room Ignored
	 * @throws JSONException If something untoward happens
	 */
	public static void do_getZoneList (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		final LinkedList <AppiusClaudiusCaecus> recipients = new LinkedList <AppiusClaudiusCaecus> ();
		recipients.add (u.getServerThread ());
		final JSONObject response = new JSONObject ();
		response.put ("from", "zoneList");
		response.put ("status", true);
		response.put ("zoneList", room.getZone ().getZoneList_JSON (u));
		try {
			final AppiusClaudiusCaecus thread = u.getServerThread ();
			if (null != thread) {
				thread.sendResponse (response);
			}
		} catch (final UserDeadException e) {
			// Default catch action, report bug (brpocock, Dec 30, 2009)
			AppiusClaudiusCaecus.reportBug (
					"Caught a UserDeadException in do_getZoneList", e);
		}
	}

	/**
	 * <p>
	 * Creates room named user/<i>user's name</i>/<i>room</i> — room is
	 * the room index number given in the JSON data as “room,” it will
	 * always be zero right now as all users have single-room houses.
	 * This will populate all furniture-type items for that room onto a
	 * set of room variables owned by the user. The user calling this
	 * method <em>must</em> be the owner of the room. If the user has
	 * not visited his/her house before, this will return an
	 * asynchronous "make a new house" notification to do the
	 * "first run" screen, by sending a message of type { "from":
	 * "initUserRoom", "status": false, "err": "showFirstRun" }.
	 * </p>
	 * <p>
	 * Success: responds with true, and "moniker": the room's moniker
	 * (user/WHOEVER/123)
	 * </p>
	 * 
	 * @param jso { room: (room-number), autoJoin: (boolean) }
	 * @param u WRITEME
	 * @param userCurrentRoomInZone WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *         from the JSON objects passed in, or conversely, if we
	 *         can't encode a response into a JSON form
	 */
	public static void do_initUserRoom (final JSONObject jso,
			final AbstractUser u,
			final AbstractRoom userCurrentRoomInZone)
	throws JSONException {
		final int roomInHouse = Integer.parseInt (jso
				.getString ("room"));
		final String roomName = "user~"
			+ u.getAvatarLabel ().toLowerCase (Locale.ENGLISH)
			+ "~" + roomInHouse;
		final AbstractRoom oldRoom = userCurrentRoomInZone.getZone ()
		.getRoomByName (roomName);
		if (null != oldRoom) {
			final JSONObject result = new JSONObject ();
			result.put ("moniker", roomName);
			u.acceptErrorReply ("initUserRoom", "exists", result,
					userCurrentRoomInZone);
			return;
		}
		Room r;
		try {
			r = Room.create (roomName,
					userCurrentRoomInZone.getZone (), false);
		} catch (final NotFoundException e1) {
			throw AppiusClaudiusCaecus
			.fatalBug ("Caught a NotFoundException in do_initUserRoom"
					+ AppiusClaudiusCaecus.stringify (e1));
		}
		r.setVariable ("homeOwner", u.getAvatarLabel ().toLowerCase (
				Locale.ENGLISH));
		r.setVariable ("m", "");
		r.setVariable ("w", "");
		r.setVariable ("s", userCurrentRoomInZone.getZone ()
				.getNextLobby ().getVariable ("s"));
		r.setVariable ("f", "userRoom.swf");
		final AbstractUser user = u;
		try {
			for (final HomeDecorItem h : user
					.getActiveDecorations (roomInHouse)) {
				h.setRoom (r);
			}
			if ("".equals (r.getVariable ("cl"))) {
				r.setVariable ("cl", "37");
			}
			if ("".equals (r.getVariable ("wl"))) {
				r.setVariable ("wl", "36");
			}
			if ("".equals (r.getVariable ("fl"))) {
				r.setVariable ("fl", "38");
			}
			try {
				final AppiusClaudiusCaecus thread = u
				.getServerThread ();
				if (null != thread) {
					thread.sendRoomList (userCurrentRoomInZone
							.getZone (), false);
				}
			} catch (final UserDeadException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}

		} catch (final NotFoundException e) {
			u.acceptErrorReply ("initUserRoom", "showFirstRun", null,
					userCurrentRoomInZone);
		}
		final JSONObject reply = new JSONObject ();
		if (jso.has ("autoJoin")) {
			reply.put ("autoJoin", jso.get ("autoJoin"));
		}
		reply.put ("moniker", roomName);
		u.acceptSuccessReply ("initUserRoom", reply,
				userCurrentRoomInZone);
	}

	/**
	 * WRITEME: document this method (brpocock, Sep 8, 2009)
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_ping (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		u.setLastActive ();
		u.acceptSuccessReply ("pong", null, room);
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @param jso { buddy: (name) } or { ignore: (name) }
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_removeFromList (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		if (jso.has ("buddy")) {
			u.removeBuddy (User.getByLogin (jso.getString ("buddy")));
		} else {
			u.attend (User.getByLogin (jso.getString ("ignore")));
		}
	}

	/**
	 * This method allows the client to “phone home”
	 * 
	 * @param jso Must contain a single string attribute named "bug."
	 *            Other attributes will be ignored.
	 * @param u The user reporting the bug.
	 * @param room The user's current room.
	 * @throws JSONException JSON encoding error
	 */
	public static void do_reportBug (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		AppiusClaudiusCaecus
		.reportClientBug ("Bug report from client:\n"
				+ "User: " + u.getAvatarLabel () + "\n"
				+ "Room " + room.getDebugName () + "\n"
				+ jso.getString ("bug"));

		final JSONObject result = new JSONObject ();
		result.put ("status", true);
		result.put ("from", "reportBug");

		try {
			final AppiusClaudiusCaecus thread = u.getServerThread ();
			if (null != thread) {
				thread.sendResponse (result);
			}
		} catch (final UserDeadException e) {
			// Don't ask; don't care
		}
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @param jso { userName = user to be reported }
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_reportUser (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		final String reportedUser = jso.getString ("userName");

		room.getZone ().tellEaves (u, room, "reporting", reportedUser);

		final AbstractUser victim = User.getByLogin (reportedUser);
		if (null == victim) return;
		victim.reportedToModeratorBy (u);

		u.acceptAdminMessage (String.format (LibMisc
				.getText ("reportedUser"), reportedUser), "Reported",
		"Lifeguard");
	}

	/**
	 * WRITEME: document this method (brpocock, Sep 2, 2009) { sender:
	 * sender, from: outOfBand, status: true, body: {JSON} } Adds
	 * "roomTitle" to body if body contains "room" and title can be
	 * determined
	 * 
	 * @param jso { to: userName, body: {JSON} } { toRoom: true, body:
	 *            {JSON} }
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_sendOutOfBandMessage (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {

		final AbstractUser to = User.getByLogin (jso.getString ("to"));
		if (null == to) return; // user does not exist

		final JSONObject body = jso.getJSONObject ("body");
		if (body.has ("room")) {
			final String roomName = body.getString ("room");
			if (roomName.startsWith ("user~")) {
				final String [] roomNameParts = roomName.split ("~");
				body.put ("roomTitle", roomNameParts [1] + "’s House");
				body.remove ("room");
				body.put ("room", "user~" + roomNameParts [1] + "~"
						+ roomNameParts [2]);
			} else {
				AbstractRoom theRoom;
				try {
					theRoom = new org.starhope.appius.game.Room (
							roomName, room.getZone ());
					body.put ("roomTitle", theRoom.getTitle ());
				} catch (final NotFoundException e) {
					body.put ("roomTitle", roomName);
				}
			}
		}

		if (jso.has ("sendRoomList")) {
			{
				final AppiusClaudiusCaecus userThread = to
				.getServerThread ();
				if (null != userThread) {
					try {
						userThread.sendRoomList ();
					} catch (final UserDeadException e) {
						// nah, nah, boo, boo
					}
				}
			}
		}

		if (jso.has ("toRoom")) {
			final AbstractRoom theRoom = room;
			for (final AbstractUser guy : theRoom.getAllUsers ()) {
				guy.acceptOutOfBandMessage (u, theRoom, body);
			}
			return;
		}

		final JSONObject reply = new JSONObject ();
		reply.put ("body", body);
		reply.put ("sender", u.getAvatarLabel ());
		u.acceptSuccessReply ("outOfBand", reply, room);

	}

	/**
	 * <p>
	 * Params: "base" and "extra" are color numbers
	 * </p>
	 * WRITEME: document this method (brpocock, Aug 27, 2009)
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 */
	public static void do_setAvatarColor (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException {
		u.setBaseColor (new Colour (jso.getLong ("base")));
		u.setExtraColor (new Colour (jso.getLong ("extra")));
		u.sendWardrobe ();
	}

	/**
	 * <p>
	 * Set or change a furniture item. To add a structural item to the
	 * room, put item: 123 without anything else. To place furniture on
	 * the floor, also add attributes x, y, and facing.
	 * </p>
	 * <p>
	 * To change furniture, replace item: with slot: (to avoid
	 * ambiguities about “which chair”)
	 * </p>
	 * <p>
	 * To remove an item from the room, send { slot: 123, remove: true }
	 * </p>
	 * 
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 * @throws NotFoundException WRITEME
	 */
	public static void do_setFurniture (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException, NotFoundException {
		HomeDecorItem item = null;
		if (jso.has ("slot")) {
			item = u.getFurnitureBySlot (jso.getInt ("slot"));
			if (jso.has ("remove") && jso.getBoolean ("remove")) {
				final HashMap <String, String> var = item
				.unsetRoom (room);
				final JSONObject vars = new JSONObject (var);

				final JSONObject result = new JSONObject ();
				result.put ("from", "roomVar");
				result.put ("status", true);
				result.put ("vars", vars);

				final LinkedList <AppiusClaudiusCaecus> recipients = new LinkedList <AppiusClaudiusCaecus> ();
				for (final AbstractUser who : room.getAllUsers ()) {
					if ( !who.isNPC ()) {
						recipients.add ( ((User) who)
								.getServerThread ());
					}
				}
				u.sendResponse (result);
				u.notifyFurnitureInventory (room);
				return;
			}
		} else {
			item = InventoryItem.pullFromInventory (
					jso.getInt ("item"), u.getUserID ())
					.asHomeDecorItem ();
		}
		if (null == item)
			throw new NotFoundException (jso.toString ());
		if (item.isFurniture ()) {
			item.setX (jso.getInt ("x"));
			item.setY (jso.getInt ("y"));
			item.setFacing (jso.getString ("facing"));
			item.setActive (true);
		} else if (item.isStructure ()) {
			u.setStructure (item);
		}
		item.setRoom (room);

		u.notifyFurnitureInventory (room);
	}

	/**
	 * Spawn an additional zone.
	 * 
	 * @param jso JSON object, containing an associative array whose
	 *            values are zones to be spawned
	 * @param u The caller responsible
	 * @param room Where is the caller?
	 * @throws JSONException if something goes awry
	 * @throws PrivilegeRequiredException if the user isn't a Developer
	 */
	@SuppressWarnings ("unchecked")
	public static void do_spawnZone (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException, PrivilegeRequiredException {
		u.assertStaffLevel (User.STAFF_LEVEL_DEVELOPER);
		final Iterator <String> keys = jso.keys ();
		final JSONObject response = new JSONObject ();
		final JSONObject zoneList = new JSONObject ();
		while (keys.hasNext ()) {
			final String zoneName = jso.getString (keys.next ());
			final AbstractZone z = room.getZone ();
			if (z instanceof Zone) {
				z.trace ("By special request of " + u.getAvatarLabel ()
						+ ", a new Zone is born: " + zoneName);
				((Zone) z).spawnZone (zoneName, "patches.png");
			} else {
				z.trace ("Attempted to spawn a zone remotely: fail");
				u
				.acceptAdminMessage (
						"Can't spawn a new zone remotely via a non-local zone",
						"Zone spawn fail", "Catullus");
			}
		}
		response.put ("newZones", zoneList);
		u.acceptSuccessReply ("spawnZone", response, room);
	}

	/**
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException Thrown if the data cannot be interpreted
	 *             from the JSON objects passed in, or conversely, if we
	 *             can't encode a response into a JSON form
	 * @throws NotFoundException WRITEME
	 */
	public static void do_speak (final JSONObject jso,
			final AbstractUser u, final AbstractRoom room)
	throws JSONException, NotFoundException {

		String filterStatus = "";
		String speech = jso.getString ("speech");

		if ( !u.canTalk () && !speech.startsWith ("/")) {
			u.sendOops ();
			return;
		}

		if (speech.length () < 1) return;

		if (speech.contains (",dumpthreads,")) {
			OpCommands.op_dumpthreads (new String [] {}, u, room);
			return;
		}

		if (speech.contains (",credits,")) {
			OpCommands.z$z (u);
			return;
		}

		if (speech.charAt (0) == '#') {
			OpCommands.exec (room, u, speech);
			return;
		} else if (speech.charAt (0) == '@') {
			try {
				u.assertStaffLevel (User.STAFF_LEVEL_STAFF_MEMBER);
			} catch (final PrivilegeRequiredException e) {
				u.sendOops ();
				return;
			}
			final String kidsName = speech.substring (1).substring (0,
					speech.indexOf (' ') - 1);
			final AbstractUser kid = User.getByLogin (kidsName);
			if (null != kid) {
				if (kid.hasStaffLevel (u.getStaffLevel ())) {
					kid.acceptAdminMessage (speech.substring (speech
							.indexOf (' ')), "", u.getAvatarLabel ());
					u.acceptAdminMessage (speech, "(sent)\n", u
							.getAvatarLabel ());
				} else {
					kid.acceptAdminMessage (speech.substring (speech
							.indexOf (' ')), "", "ADMIN");
					u.acceptAdminMessage (speech, "(sent)\n", "ADMIN");
					room.getZone ().tellEaves (u, room, "admin",
							speech.substring (speech.indexOf (' ')));
				}
			} else {
				u.acceptAdminMessage ("Can't find a kid named "
						+ kidsName, "@message", "Catullus");
			}
			return;
		}

		speech = Commands.handleDice (speech);

		final FilterResult carlSays = Zone.censor
		.filterMessage (speech);

		switch (carlSays.status) {
		case Ok:
			filterStatus = "said";
			break;
		case Black:
			u.sendOops ();
			filterStatus = "Oops!";
			speech = "";
			break;
		case Red:
			if (AppiusConfig.confDontKickStaff ()
					&& u.getStaffLevel () > User.STAFF_LEVEL_PUBLIC) {
				u.acceptAdminMessage (LibMisc.getText ("kickLang")
						+ "\n\n(Protected bt Staff Of Protection)",
						"Redlisted word hit", "God");
			} else {
				try {
					u.kick (User.getByID (1), "obs", 15);
				} catch (final PrivilegeRequiredException e) {
					AppiusClaudiusCaecus.fatalBug (e);
				}
			}
			filterStatus = "REDLIST";
			speech = "";
			break;
		default:
			filterStatus = "¿que?";
		}

		System.out.printf (
				" (, ) %30s %-40s %10s\n      @%29s %-40s\n", u
				.getAvatarLabel (), jso.get ("speech"),
				filterStatus, room.getName (), speech);

		room.getZone ().tellEaves (u, room, filterStatus, speech);

		room.sendPublicMessage (u, speech);
	}

	/**
	 * Create a fancy signature thing to validate buddy list requests
	 * 
	 * @param u WRITEME
	 * @param u2 WRITEME
	 * @return WRITEME
	 */
	private static String getBuddySignature (final AbstractUser u,
			final AbstractUser u2) {
		MessageDigest stomach = null;
		try {
			stomach = MessageDigest.getInstance ("SHA1");
		} catch (final NoSuchAlgorithmException e1) {
			throw AppiusClaudiusCaecus.fatalBug (e1);
		}
		stomach.reset ();
		try {
			stomach
			.update ( (String.valueOf (u.getUserID ()) + "$" + String
					.valueOf (u2.getUserID ()))
					.getBytes ("US-ASCII"));
		} catch (final UnsupportedEncodingException e) {
			AppiusClaudiusCaecus.blather ("can't go 16 bit");
		}
		return LibMisc.hexify (stomach.digest ());
	}

	/**
	 * TODO: document this method (brpocock, Dec 10, 2009)
	 * 
	 * @return WRITEME
	 */
	public static String getRev () {
		return "$Rev: 2587 $";
	}

	/**
	 * Handle die rolls, coin tosses, and magic Rock-Paper-Scissors
	 * picker
	 * 
	 * @param inSpeech Speech before filtering
	 * @return Speech after filtering
	 */
	static String handleDice (final String inSpeech) {
		String speech = inSpeech;
		if ("/coin".equals (speech)) {
			// filterStatus = "cointoss";
			if (Math.random () > 0.5) {
				speech = "/coinheads";
			} else {
				speech = "/cointails";
			}
		} else if ("/rps".equals (speech)) {
			// filterStatus = "rockpaperscissors";
			if (Math.random () > 1 / 3) {
				speech = "/rock";
			} else if (Math.random () > 1 / 2) {
				speech = "/paper";
			} else {
				speech = "/scissors";
			}
		} else if ("/dice".equals (speech)) {
			// filterStatus = "die rolled";
			final int rolled = (int) (Math.random () * 6 + 1);
			switch (rolled) {
			case 1:
				speech = "/diceone";
				break;
			case 2:
				speech = "/dicetwo";
				break;
			case 3:
				speech = "/dicethree";
				break;
			case 4:
				speech = "/dicefour";
				break;
			case 5:
				speech = "/dicefive";
				break;
			case 6:
				speech = "/dicesix";
				break;
			default:
				AppiusClaudiusCaecus
				.reportBug ("Die landed on a corner.");
			}
		}
		return speech;
	}

}
