/**
 * <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.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArrayList;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.GameLogicException;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.except.PrivilegeRequiredException;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.mb.Messages;
import org.starhope.appius.messaging.AbstractCensor;
import org.starhope.appius.types.AbstractZone;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.User;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.appius.via.RemoteZone;
import org.starhope.appius.via.ViaAppia;
import org.starhope.util.LibMisc;

import com.tootsville.game.ShaddowFalls;
import com.tootsville.game.ZapAttack;
import com.tootsville.hangman.Censor;
import com.tootsville.user.Toot;

/**
 * @author brpocock
 */
@SuppressWarnings ("deprecation")
public class Zone implements AbstractZone, ViaAppia <RemoteZone>,
Comparable <AbstractZone> {

	/**
	 * Censorship object (Hangman) censor (OsirisPure)
	 */
	public static final AbstractCensor censor = new Censor ();

	/**
	 * List of empty zones
	 */
	protected final static Set <AbstractZone> emptyZones = new ConcurrentSkipListSet <AbstractZone> ();

	/**
	 * <p>
	 * The password for $Eaves is the medieval Tuscan interpretation of
	 * the inscription upon the Black Gates, as read by Virgil to Dante
	 * Alligheri in The Divine Comedy.
	 * </p>
	 * <p>
	 * Perhaps the best English translation thereof is:
	 * </p>
	 * <div>Lay down all hope, you that go in by me.</div> -- Dorothy L.
	 * Sayers
	 */
	// private static final String EAVESDROP_PASSWORD =
	// "Lasciate ogne speranza, voi ch-intrate";
	/**
	 * 
	 */
	static long lastCheckedZonesForSpawn = 0;

	/**
	 * Time between updates for NPC's
	 */
	private static int NPC_TICK_INTERVAL = Integer
	.parseInt (AppiusConfig.getConfigOrDefault (
			"org.starhope.appius.npc.tickSeconds", "30"));

	/**
	 * keep the random source around for fun.
	 */
	private static Random randomSource = new Random ();

	/**
	 * Set of retired zones
	 */
	static Set <AbstractZone> retiredZones = new ConcurrentSkipListSet <AbstractZone> ();

	/**
	 * Java Serialization unique ID
	 */
	private static final long serialVersionUID = -2338918747157284836L;

	/**
	 * The system “God” user account
	 */
	// static God systemUser = null;
	/**
	 * Max users allowed into one room of an user's house at a time
	 */
	private static int USER_ROOM_MAX_USERS = Integer
	.parseInt (AppiusConfig.getConfigOrDefault (
			"org.starhope.appius.userRoom.maxUsers", "10"));
	/**
	 * Get the percentage of "full" at which we consider a zone to be
	 * full enough to warrant spawning new ones.
	 */
	private static double ZONE_FULL_RATIO = Double
	.parseDouble (AppiusConfig.getConfigOrDefault (
			"org.starhope.appius.zones.fullRatio", "0.8"));
	/**
	 * Get the percentage of "full" at which we consider a zone to be
	 * light enough that it's almost empty
	 */
	static double ZONE_LIGHT_RATIO = Double.parseDouble (AppiusConfig
			.getConfigOrDefault (
					"org.starhope.appius.zones.lightRatio", "0.2"));

	/**
	 * The max. users per Zone (And per every room in every real zone)
	 */
	private static int ZONE_MAX_USERS = Integer.parseInt (AppiusConfig
			.getConfigOrDefault ("org.starhope.appius.zones.maxUsers",
			"500"));

	/**
	 * Time (in seconds) between spawning new zones
	 */
	private static int ZONE_SPAWN_SECONDS = Integer
	.parseInt (AppiusConfig.getConfigOrDefault (
			"org.starhope.appius.zones.spawnSeconds", "30"));

	/**
	 * 
	 */
	public static void configUpdated () {
		Zone.NPC_TICK_INTERVAL = AppiusConfig.getIntOrDefault (
				"org.starhope.appius.npc.tickSeconds", 30);

		Zone.USER_ROOM_MAX_USERS = AppiusConfig.getIntOrDefault (
				"org.starhope.appius.userRoom.maxUsers", 10);
		Zone.ZONE_FULL_RATIO = Double.parseDouble (AppiusConfig
				.getConfigOrDefault (
						"org.starhope.appius.zones.fullRatio", "0.8"));
		Zone.ZONE_LIGHT_RATIO = Double.parseDouble (AppiusConfig
				.getConfigOrDefault (
						"org.starhope.appius.zones.lightRatio", "0.8"));
		Zone.ZONE_MAX_USERS = Integer.parseInt (AppiusConfig
				.getConfigOrDefault (
						"org.starhope.appius.zones.maxUsers", "500"));
		Zone.ZONE_SPAWN_SECONDS = Integer
		.parseInt (AppiusConfig.getConfigOrDefault (
				"org.starhope.appius.zones.spawnSeconds", "30"));
	}

	/**
	 * @param name the name of the Zone to find
	 * @return the Zone with that name (if any)
	 * @deprecated use {@link AppiusClaudiusCaecus#getZone(String)}
	 */
	@Deprecated
	public static Zone getByName (final String name) {
		return AppiusClaudiusCaecus.getZone (name);
	}

	/**
	 * @return Seconds between NPC updates
	 */
	public static int getNPCTickInterval () {
		return Zone.NPC_TICK_INTERVAL;
	}

	/**
	 * This is a breakout from {@link #checkZonesForSpawn()} to
	 * determine the number of zones that need to be spawned or removed
	 * 
	 * @param zones The set of all Zones in the multiverse
	 * @return the (signed) number of zones to be created (positive) or
	 *         removed (negative)
	 */
	private static int getNumberOfZonesNeeded (
			final LinkedList <AbstractZone> zones) {
		int fullZones = 0;
		int lightZones = 0;
		final Iterator <AbstractZone> zoneIterator = zones.iterator ();
		while (zoneIterator.hasNext ()) {
			final AbstractZone z = zoneIterator.next ();
			if ('$' != z.getName ().charAt (0)
					&& !Zone.retiredZones.contains (z)) {
				AppiusClaudiusCaecus.blather ("+ZONE: " + z.getName ()
						+ " with " + z.getAllUsersInZone ().size ()
						+ " users out of a max of " + z.getMaxUsers ());
				final int numUsers = z.getAllUsersInZone ().size ();
				if (numUsers > z.getMaxUsers ()
						* Zone.getZoneFullRatio ()) {
					++fullZones;
				} else if (numUsers == 0) {
					Zone.emptyZones.add (z);
				} else if (numUsers < z.getMaxUsers ()
						* Zone.getZoneLightRatio ()) {
					++lightZones;
				}
			}
		}

		final int needZones = fullZones
		- (lightZones + Zone.emptyZones.size ());
		return needZones;
	}

	/**
	 * @return get the Subversion revision of this file
	 */
	public static String getRev () {
		return "$Rev: 2587 $";
	}

	/**
	 * @return get the max number of users allowed in an user's
	 *         house/rooms
	 */
	public static int getUserRoomMaxUsers () {
		return Zone.USER_ROOM_MAX_USERS;
	}

	/**
	 * @return the ratio of its capacity at which we declare that a zone
	 *         is “full,” and do not allow it to accept more users. Each
	 *         lightly-filled or empty zone counteracts a full zone when
	 *         deciding whether to spawn additional zones.
	 */
	private static double getZoneFullRatio () {
		return Zone.ZONE_FULL_RATIO;
	}

	/**
	 * @return the ratio of its capacity at which we declare that a zone
	 *         is “lightly used,” and assume that it can absorb more
	 *         users. Each lightly-filled or empty zone counteracts a
	 *         full zone when deciding whether to spawn additional
	 *         zones.
	 */
	private static double getZoneLightRatio () {
		return Zone.ZONE_LIGHT_RATIO;
	}

	/**
	 * @return get the maximum number of users allowed in a zone
	 */
	public static int getZoneMaxUsers () {
		return Zone.ZONE_MAX_USERS;
	}

	/**
	 * Get the number of seconds between checking for zone spawning
	 * 
	 * @return The interval between checks for spawning zones
	 */
	public static int getZoneSpawnSeconds () {
		return Zone.ZONE_SPAWN_SECONDS;
	}

	/**
	 * Badges are icons placed upon the map to indicate something
	 * special about a room
	 */
	private transient final Map <String, AbstractRoom> badges = new ConcurrentHashMap <String, AbstractRoom> ();

	/**
	 * Rooms to be culled
	 */
	protected transient final Set <String> cullRooms = new ConcurrentSkipListSet <String> ();

	/**
	 * Lobby rooms into which users are dropped at login, if they don't
	 * specify an initial room to join on their own
	 */
	private transient final List <AbstractRoom> lobbies = new CopyOnWriteArrayList <AbstractRoom> ();

	/**
	 * Names of empty zones
	 */
	protected transient final Set <String> myEmptyZones = new ConcurrentSkipListSet <String> ();

	/**
	 * The name of this zone
	 */
	private final String myName;

	/**
	 * The hostname of the server on which this Zone is running
	 */
	private String myServer;

	/**
	 * The port number of the server on which this Zone is running
	 */
	private int myServerPort;

	/**
	 * The room numbers for dynamic rooms begin here
	 */
	private int nextDynamicRoomNumber = 100;

	/**
	 * All rooms in the Zone: sorted by ID.
	 * 
	 * @see #roomsByMoniker
	 */
	private final ConcurrentHashMap <Integer, Room> roomsByID = new ConcurrentHashMap <Integer, Room> ();

	/**
	 * All rooms in the Zone: sorted by moniker.
	 * 
	 * @see #roomsByID
	 */
	private final ConcurrentHashMap <String, Room> roomsByMoniker = new ConcurrentHashMap <String, Room> ();

	/**
	 * A flag set once the server has indicated that it's ready to go,
	 * to avoid issues with events firing off too soon (e.g. NPC's)
	 */
	protected boolean serverReady;

	/**
	 * the set of user ID's of all users in the Zone. We don't keep User
	 * objects here because we were once having instantiation failures;
	 * it would probably be more efficient to do so, but XXX
	 */
	private final ConcurrentSkipListSet <Integer> usersInZone = new ConcurrentSkipListSet <Integer> ();

	/**
	 * @param zoneName The name for the new Zone
	 */
	public Zone (final String zoneName) {
		myName = zoneName;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#activate()
	 */
	public void activate () {
		AppiusClaudiusCaecus.addZone (myName, this);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#add(AbstractRoom)
	 */
	public void add (final AbstractRoom room) {
		if ( ! (room instanceof Room))
			throw AppiusClaudiusCaecus
			.fatalBug ("Can't add remote room to local zone: "
					+ room.toString ());
		final Integer id = room.getID ();
		final String moniker = room.getMoniker ();

		if (roomsByID.containsKey (room.getID ())) {
			final String errStr = "Attempting to replace "
				+ room.getName ();
			// OpCommands.op_wallops (new String [] {
			// errStr }, User
			// .getByID (1), room.getID (), this);
			AppiusClaudiusCaecus.reportBug (errStr);
			return;
		}
		if (roomsByMoniker.containsKey (room.getMoniker ())) {
			final String errStr = "Attempting to replace (moniker match) "
				+ room.getName ();
			// OpCommands.op_wallops (new String [] {
			// errStr }, User
			// .getByID (1), room.getID (),
			// this);
			AppiusClaudiusCaecus.reportBug (errStr);
			return;
		}

		trace ("Adding room " + moniker + " (#" + id + ")");

		roomsByID.put (id, (Room) room);
		roomsByMoniker.put (moniker, (Room) room);
	}

	/**
	 * @param user the user entering the zone
	 */
	public void add (final AbstractUser user) {
		usersInZone.add (user.getUserID ());
		sendBadges (user);
	}

	/**
	 * Assert that the given user must have a given level of staff
	 * privileges
	 * 
	 * @param u The Smart Fox user object
	 * @param staffLevelRequired The staff level required to perform the
	 *            action
	 * @throws PrivilegeRequiredException if the user
	 */
	public void assertStaffLevel (final User u,
			final int staffLevelRequired)
	throws PrivilegeRequiredException {
		if (u.getStaffLevel () < staffLevelRequired)
			throw new PrivilegeRequiredException (staffLevelRequired);
	}

	/**
	 * Notify everyone in the zone that the badges have been changed
	 */
	private void badgesChanged () {
		final JSONObject notify = getAllBadges_JSON ();
		for (final AbstractUser u : getAllUsersInZone ()) {
			try {
				sendSuccessReply ("badgeUpdate", notify, u, u
						.getRoomNumber ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.reportBug (
						"Caught a JSONException in badgesChanged", e);
			}
		}
	}

	/**
	 * Deprecated method. This method will be removed soon. Send a biff
	 * message to announce the number of message in a user's Inbox.
	 * 
	 * @param user A Tootsville™ user
	 * @param room The room in which the user is standing
	 * @throws JSONException If the biff can't be sent
	 * @deprecated Use
	 *             {@link com.tootsville.user.Toot#biff(AbstractZone, AbstractRoom)}
	 *             instead
	 */
	@Deprecated
	public void biff (final Toot user, final AbstractRoom room)
	throws JSONException {
		user.biff (this, room);
	}

	/**
	 * See if enough zones are full to warrant spawning a new one; or,
	 * if zones are empty and can be deallocated.
	 */
	synchronized void checkZonesForSpawn () {
		final long now = System.currentTimeMillis ();
		if (Zone.lastCheckedZonesForSpawn > now
				- Zone.getZoneSpawnSeconds () * 1000L) return;

		cullUserRooms ();

		trace ("Checking zones for spawn");

		Zone.emptyZones.clear ();
		final LinkedList <AbstractZone> zones = AppiusClaudiusCaecus
		.getAllZones ();

		if (zones.size () == 0) {
			trace ("It is always dark in the beginning.");
			spawnZone ("Zap", "lightning");
			return;
		}

		final int needZones = Zone.getNumberOfZonesNeeded (zones);
		if (needZones > 0) {
			trace ("I need to spawn " + needZones + " zones.");
			for (int i = 0; i < needZones; ++i) {
				spawnNewZone ();
			}
		} else if (needZones == 0) {
			trace ("The number of zones is just right.");
		} else {
			trace ("There are about " + -needZones + " too many zones.");
			for (final AbstractZone emptyZone : Zone.emptyZones) {
				if (emptyZone.getName ().equals ("Zap")) {
					trace ("Zap is empty, but I refuse to destroy it.");
					Zone.emptyZones.remove (emptyZone);
				} else {
					trace ("I ought to destroy the empty zone "
							+ emptyZone.getName ());
				}
			}
		}
		if (Zone.emptyZones.size () > 1) {
			trace ("Removing " + Zone.emptyZones.size ()
					+ " empty zone(s).");
			final AbstractZone firstEmptyZone = (AbstractZone) Zone.emptyZones
			.toArray () [0];
			trace ("There's lots of empty zones; preserving "
					+ firstEmptyZone.getName ());
			Zone.emptyZones.remove (firstEmptyZone);
		}

		Zone.lastCheckedZonesForSpawn = now;

		myEmptyZones.clear ();
		final Iterator <AbstractZone> emptyZone = Zone.emptyZones
		.iterator ();
		while (emptyZone.hasNext ()) {
			myEmptyZones.add (emptyZone.next ().getName ());
		}

		trace ("Zones good to go");
	}

	/**
	 * @see org.starhope.appius.types.AbstractZone#clearAllBadges()
	 */
	public void clearAllBadges () {
		badges.clear ();
		badgesChanged ();
	}

	/**
	 * clear all badges on one room
	 * 
	 * @param room the room upon which all badges are to be cleared
	 */
	public void clearAllBadges (final AbstractRoom room) {
		for (final Entry <String, AbstractRoom> badge : badges
				.entrySet ()) {
			if (badge.getValue ().equals (room)) {
				badges.remove (badge.getKey ());
			}
		}
		badgesChanged ();
	}

	/**
	 * clear the given badge name off of any room to which it might be
	 * applied
	 * 
	 * @param string the badge name
	 * @throws GameLogicException if the given badge wasn't set on the
	 *             room already
	 */
	public void clearBadge (final String string)
	throws GameLogicException {
		badges.remove (string);
		badgesChanged ();
	}

	/**
	 * @param other the other zone
	 * @return the relative ordering of the zones, in Unicode order by
	 *         name
	 */
	public int compareTo (final AbstractZone other) {
		if (null == other) return -1;
		return getName ().compareTo (other.getName ());
	}

	/**
	 * remove unused user rooms
	 */
	protected void cullUserRooms () {
		for (final Room room : roomsByMoniker.values ()) {
			final String ownerName = room.getVariable ("homeOwner");
			if (null != ownerName && !"".equals (ownerName)) {
				final AbstractUser roomOwner = User
				.getByLogin (ownerName);
				if (null != roomOwner) {
					if ( !roomOwner.isOnline ()
							&& room.getAllUsers ().size () == 0) {
						destroyRoom (room);
					}
				}
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#destroy()
	 */
	public void destroy () {
		trace ("And there was great weeping, and gnashing of teeth.");
		// this is only fired in $Eden, it looks like...
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#destroyRoom(AbstractRoom)
	 */
	public void destroyRoom (final AbstractRoom room) {
		if ( !room.getZone ().equals (this))
			throw AppiusClaudiusCaecus
			.fatalBug ("Destroying other peoples' rooms is not nice:"
					+ room.toString ());
		final Integer roomID = room.getID ();
		final String roomMoniker = room.getMoniker ();
		room.destroySelf ();
		roomsByID.remove (roomID);
		roomsByMoniker.remove (roomMoniker);
	}

	/**
	 * Drop the user's house from the Zone
	 * 
	 * @param user the user whose house is to be dropped
	 */
	private void dropUser (final User user) {
		cullRooms.add (user.getName ());
	}

	/**
	 * determine whether two Zone pointers are the same object
	 * 
	 * @param z another Zone
	 * @return true, if the two objects are the same Zone
	 */
	public boolean equals (final AbstractZone z) {
		return getURL ().equals (z.getURL ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals (final Object obj) {
		if (obj instanceof AbstractZone) return equals ((AbstractZone) obj);
		return false;
	}

	/**
	 * Generate the name for a new zone based upon this one's name
	 * 
	 * @return a new zone name
	 */
	private String genZoneName () {
		final String [] myNameParts = getName ().split (" ");
		final String myLastPart = myNameParts [myNameParts.length - 1];
		int myNumber = 1;
		String [] myWords;
		try {
			myNumber = Integer.parseInt (myLastPart);
			myWords = Arrays.copyOfRange (myNameParts, 0,
					myNameParts.length - 2);
		} catch (final NumberFormatException e) {
			myWords = myNameParts;
		}
		final StringBuilder myPrefix = new StringBuilder ();
		for (final String word : myWords) {
			myPrefix.append (word);
			myPrefix.append (' ');
		}
		final String myPrefix$ = myPrefix.toString ();
		while (true) {
			++myNumber;
			if (null == AppiusClaudiusCaecus.getZone (myPrefix$
					+ myNumber))
				return myPrefix$ + myNumber;
		}
	}

	/**
	 * Get all badges in this Zone (and to which rooms they are applied)
	 * 
	 * @return a map of badges and room monikers
	 */
	public Map <String, String> getAllBadges () {
		final HashMap <String, String> all = new HashMap <String, String> ();
		for (final Entry <String, AbstractRoom> badge : badges
				.entrySet ()) {
			if (null != badge && null != badge.getKey ()
					&& null != badge.getValue ()) {
				all.put (badge.getKey (), badge.getValue ()
						.getMoniker ());
			}
		}
		return all;
	}

	/**
	 * get all badges on the Zone in JSON form
	 * 
	 * @return the JSON set describing all active badges
	 */
	private JSONObject getAllBadges_JSON () {
		final JSONObject notify = new JSONObject ();
		final JSONObject badgeSet = new JSONObject ();
		for (final Entry <String, String> badge : getAllBadges ()
				.entrySet ()) {
			try {
				badgeSet.put (badge.getKey (), badge.getValue ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.reportBug (
						"Caught a JSONException in badgesChanged", e);
			}
		}
		try {
			notify.put ("badges", badgeSet);
		} catch (final JSONException e1) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in badgesChanged", e1);
		}
		return notify;
	}

	/**
	 * get the user ID's of all users active in the Zone
	 * 
	 * @return the set of all users in the Zone
	 */
	public Set <Integer> getAllUsersIDsInZone () {
		return new HashSet <Integer> (usersInZone);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getAllUsersInZone()
	 */
	public Collection <AbstractUser> getAllUsersInZone () {
		final Collection <AbstractUser> userList = new HashSet <AbstractUser> ();
		for (final Integer id : usersInZone) {
			final AbstractUser user = User.getByID (id);
			if (null == user) {
				usersInZone.remove (id);
			} else if ( !user.isOnline ()) {
				usersInZone.remove (id);
			} else if (user.getZone () != this) {
				usersInZone.remove (id);
			} else if (user != User.getByID (id)) {
				AppiusClaudiusCaecus
				.reportBug ("User in usersInZone doesn't match user from getByID for ID="
						+ id);
			} else {
				userList.add (user);
			}
		}
		return userList;
	}

	/**
	 * Get the “apple” (CHAP authentication string SHA1 digest encoded
	 * in hex) for the login system
	 * 
	 * @param serverThread The AppiusClaudiusCaecus to the relevant user
	 * @param pass The plaintext password to be used
	 * @return the “apple” string
	 * @see AppiusClaudiusCaecus#getApple(String)
	 */
	@Deprecated
	private String getApple (final AppiusClaudiusCaecus serverThread,
			final String pass) {
		return serverThread.getApple (pass);
	}

	/**
	 * Get any badges assigned to a room
	 * 
	 * @param room the room in question
	 * @return the set of badge strings
	 */
	public Set <String> getBadgesForRoom (final AbstractRoom room) {
		final HashSet <String> set = new HashSet <String> ();
		for (final Entry <String, AbstractRoom> badge : badges
				.entrySet ()) {
			if (badge.getValue ().equals (room)) {
				set.add (badge.getKey ());
			}
		}
		return set;
	}

	/**
	 * @return get the next room number to be used for a dynamic room
	 */
	public int getDynamicRoomNumber () {
		return nextDynamicRoomNumber++ ;
	}

	/**
	 * Get the host on which this zone's server is running
	 * 
	 * @return the host on which this Zone's server is running
	 */
	public String getHost () {
		return AppiusClaudiusCaecus.getServerHostname ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getMaxUsers()
	 */
	public int getMaxUsers () {
		return Zone.getZoneMaxUsers ();
	}

	/**
	 * @return {@link #myServer}
	 */
	public String getMyServer () {
		return myServer;
	}

	/**
	 * @return {@link #myServerPort}
	 */
	public int getMyServerPort () {
		return myServerPort;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getName()
	 */
	public String getName () {
		return myName;
	}

	/**
	 * <p>
	 * Get the next room which will accept an inbound user as the next
	 * lobby.
	 * </p>
	 * <p>
	 * FIXME: This contains Tootsville-specific code. There should
	 * instead be a room variable to indicate that a room is a lobby.
	 * </p>
	 * <p>
	 * FIXME: This code isn't used in Tootsville any more, anyways.
	 * Tootsville users get the map screen after logging in.
	 * </p>
	 * 
	 * @return The name of the room into which the user should join
	 */
	public AbstractRoom getNextLobby () {
		if (AppiusConfig
				.getConfigBoolOrFalse ("com.tootsville.disperseLogins")) {
			final AbstractRoom square = getRoomByName ("tootSquare");
			if (square.getUserCount () < AppiusConfig.getIntOrDefault (
					"com.tootsville.tootSquareBusy", 20))
				return getRoomByName ("tootSquare");
			if (Zone.randomSource.nextBoolean ())
				return getRoomByName ("tootSquareWest");
			return getRoomByName ("tootUniversity");
		}
		return getRoomByName ("tootSquare");
	}

	/**
	 * Get the port number on which this zone's server is running
	 * 
	 * @return the port number on which this Zone's server is running
	 */
	public int getPort () {
		return AppiusClaudiusCaecus.getServerPort ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getRoom(java.lang.Integer)
	 */
	public Room getRoom (final Integer room) {
		return roomsByID.get (room);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getRoomByName(java.lang.String)
	 */
	public Room getRoomByName (final String string) {
		return roomsByMoniker.get (string);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getRoomList()
	 */
	public Collection <AbstractRoom> getRoomList () {
		final Collection <AbstractRoom> ret = new HashSet <AbstractRoom> ();
		ret.addAll (roomsByID.values ());
		return ret;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getRoomListSFSXML()
	 */
	public String getRoomListSFSXML () {
		final StringBuilder sb = new StringBuilder ();
		sb.append ("<msg t='sys'><body action='rmList' r='0'><rmList>");
		trace (" Getting room list for " + myName + " with "
				+ roomsByID.size () + " rooms ");
		for (final AbstractRoom room : roomsByID.values ()) {
			sb.append ("<rm id='");
			sb.append (room.getID ());
			sb.append ("' priv='0' temp='0' game='0' ucnt='");
			sb.append (room.getUserCount ());
			sb.append ("' maxu='");
			sb.append (room.getMaxUsers ());
			sb.append ("' maxs='0'><n><![CDATA[");
			sb.append (room.getMoniker ());
			sb.append ("]]></n></rm>");
		}
		sb.append ("</rmList></body></msg>");
		return sb.toString ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getRoomMaxUsers()
	 */
	public int getRoomMaxUsers () {
		return getMaxUsers ();
	}

	/**
	 * @return the system's own user account
	 * @deprecated unused?
	 */
	@Deprecated
	public synchronized User getSystemUser () {
		return null;
	}

	/**
	 * TODO: document this method (brpocock, Jan 4, 2010)
	 * 
	 * @return an URL for this Zone
	 */
	public String getURL () {
		return "appius://" + getMyServer () + ":" + getMyServerPort ()
		+ "/" + getName ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getUserByName(java.lang.String)
	 */
	public AbstractUser getUserByName (final String buddy) {
		final int id = User.getIDForLiveUserName (buddy);
		if (id <= 0) return null;
		if ( !usersInZone.contains (id)) return null;
		// this should be guaranteed to hit the liveCache
		return User.getByLogin (buddy);
	}

	/**
	 * @return {@link #getAllUsersInZone()}
	 */
	@Deprecated
	public Collection <AbstractUser> getUserList () {
		return getAllUsersInZone ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#getUserRoom(AbstractUser)
	 */
	public AbstractRoom getUserRoom (final AbstractUser user) {
		for (final AbstractRoom room : roomsByID.values ()) {
			if (room.contains (user)) return room;
		}
		return null;
	}

	/**
	 * Gets the Zone data. The user passed in will be used to compute
	 * the “buddies” counter for the zone
	 * 
	 * @param user the user from whose perspective we're collecting the
	 *        zone information
	 * @return JSON data <br />
	 *         { name: <i>zone name</i>, <br />
	 *         &nbsp; host: <i>server hostname or IP address</i>, <br />
	 *         &nbsp; usersOn: <i>total number of active users</i>, <br />
	 *         &nbsp; maxUsers: <i>maximum number of users allowed</i>, <br />
	 *         &nbsp; bg: <i>zone icon background image</i>, <br />
	 *         &nbsp; assetPath: <i>path to resolve assets; default is
	 *         ""</i>, <br />
	 *         &nbsp; buddies: <i>number of online buddies for the
	 *         selected user</i> <br />
	 * @throws JSONException if something can't be encoded in JSON
	 */
	public JSONObject getZoneData_JSON (final AbstractUser user)
	throws JSONException {
		final String zoneName = getName ();
		final JSONObject zoneData = new JSONObject ();
		zoneData.put ("name", zoneName);
		zoneData.put ("host", AppiusConfig.getServerName ());
		zoneData.put ("usersOn", Integer.valueOf (
				getAllUsersInZone ().size ()).toString ());
		zoneData.put ("maxUsers", Integer.valueOf (getMaxUsers ())
				.toString ());
		final Room eaves = getRoomByName ("$Eaves");
		if (null != eaves) {
			final String background = eaves.getVariable ("zoneImage");
			zoneData.put ("bg", background);
		}
		zoneData.put ("assetPath", "");
		int buddies = 0;

		for (final String buddy : user.getBuddyListNames ()) {
			final AbstractUser bud = getUserByName (buddy);
			if (null != bud && bud.isOnline ()
					&& null != bud.getZone ()
					&& bud.getZone ().getName ().equals (zoneName)) {
				++buddies;
			}
		}

		zoneData.put ("buddies", String.valueOf (buddies));
		return zoneData;
	}

	/**
	 * Get the set of all zones active (and not hidden nor retired) in
	 * JSON form. Hidden zones begin with a “$”
	 * 
	 * @param user the user whose buddy list will be used to get the
	 *        buddy counts on each zone
	 * @return a zoneList object to be passed to the client
	 */
	public synchronized JSONObject getZoneList_JSON (
			final AbstractUser user) {
		/*
		 * Generate the zone list
		 */
		final JSONObject zoneList = new JSONObject ();
		final LinkedList <AbstractZone> zones = AppiusClaudiusCaecus
		.getAllZones ();
		final Iterator <AbstractZone> zoneIterator = zones.iterator ();

		int i = 0;
		while (zoneIterator.hasNext ()) {
			final AbstractZone z = zoneIterator.next ();

			final String zoneName = z.getName ();
			if (zoneName.charAt (0) != '$'
				&& !myEmptyZones.contains (zoneName)) {
				try {
					zoneList.put (String.valueOf (i++ ), z
							.getZoneData_JSON (user));
				} catch (final JSONException e) {
					AppiusClaudiusCaecus.reportBug (e);
				}
			}

		}
		return zoneList;
	}

	/**
	 * Process a login request from the user
	 * 
	 * @param zoneName Unused. Should be this zone's name, but the
	 *            parameter is ignored.
	 * @param bigNick The user's requested nickname (attempted user
	 *            name)
	 * @param password This is a bit of a misnomer. We actually are
	 *            checking for the secret key (CHAP cookie) for the
	 *            current channel, to which has been appended the user's
	 *            actual password, as presented as a hex-coded SHA1
	 *            digest. (In brief: pseudocode of sha1( cookie +
	 *            password ).toHex )
	 * @param serverThread This is the thread over which we are
	 *            communicating with the prospective user
	 * @return boolean? WRITEME
	 */
	@Deprecated
	public boolean handleLogin (final String zoneName,
			final String bigNick, final String password,
			final AppiusClaudiusCaecus serverThread) {
		return serverThread.logIn (this, bigNick, password);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @param cmd WRITEME
	 * @param jso WRITEME
	 * @param u WRITEME
	 * @param fromRoom WRITEME
	 */
	public void handleRequest (final String cmd, final JSONObject jso,
			final User u, final int fromRoom) {
		trace ("XTN: " + u.getName () + "@" + fromRoom + "@" + myName
				+ " “" + cmd + "” (JSO) " + jso.toString ());

		if (AppiusConfig
				.getConfigBoolOrFalse ("org.starhope.appius.smartFaux.traceRoomDisjoints"))
			if (0 < fromRoom && u.getRoomNumber () != fromRoom) {
				AppiusClaudiusCaecus.reportBug ("User “"
						+ u.getUserName () + "” (#" + u.getUserID ()
						+ ") is in room #" + u.getRoomNumber ()
						+ " but sending packets for room #" + fromRoom
						+ "!");
			}

		u.getServerThread ().commandJSON (cmd, jso, Commands.class);
	}

	/**
	 * Once the server indicates its readiness, begin spawning
	 * auto-spawning Zones
	 */
	synchronized void handleServerReady () {
		if (myName.length () == 0) {
			trace ("Unnamed zone already reports readiness");
		}
		trace ("Now ready in zone " + myName);
		trace ("(re-)loading censorship for " + myName);

		try {
			Censor.loadLists (AppiusConfig.getDatabaseConnection ());
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}

		serverReady = true;
		if ("$Eden".equals (myName)) {
			trace ("In the beginning, the Zones were void (¿or was it null or undef?).");
			spawnZone ("Zap", "lightning");
		}

	}

	/**
	 * @param room the room in which the speech is occurring
	 * @param u the speaker
	 * @param toSpeak WRITEME
	 * @see AbstractUser#speak(AbstractRoom,String)
	 * @deprecated use {@link AbstractUser#speak(AbstractRoom, String)}
	 */
	@Deprecated
	protected void handleSpeak (final AbstractRoom room,
			final AbstractUser u, final String toSpeak) {
		if (null == u) return;
		u.speak (room, toSpeak);
	}

	/**
	 * @see org.starhope.appius.types.AbstractZone#handleSpeak(int,AbstractUser,String)
	 * @deprecated use {@link AbstractUser#speak(AbstractRoom, String)}
	 */
	@Deprecated
	public void handleSpeak (final int roomNumber,
			final AbstractUser user, final String speech) {
		user.speak (roomsByID.get (roomNumber), speech);
	}

	/**
	 * Handle a user speaking. This redirects through to
	 * {@link AbstractUser#speak(AbstractRoom, String)}
	 * 
	 * @param roomNum The room ID number
	 * @param speaker The user speaking
	 * @param toSpeak The text to be spoken
	 * @deprecated use {@link AbstractUser#speak(AbstractRoom, String)}
	 */
	@Deprecated
	public void handleSpeak (final Integer roomNum,
			final AbstractUser speaker, final String toSpeak) {
		handleSpeak (roomsByID.get (roomNum), speaker, toSpeak);
	}

	/**
	 * WRITEME: document this method (brpocock, Jul 14, 2009)
	 * 
	 * @param user WRITEME
	 */
	public void handleUserLost (final User user) {
		dropUser (user);
		checkZonesForSpawn ();
	}

	/**
	 * WRITEME: document this method (brpocock, Jul 14, 2009)
	 * 
	 * @param zone WRITEME
	 * @param room WRITEME
	 * @param uid WRITEME
	 * @param user WRITEME
	 * @param oldPlayerIndex WRITEME
	 */
	public void handleUserPart (final String zone,
			final AbstractRoom room, final String uid, final User user,
			final String oldPlayerIndex) {
		if ( !zone.equals (myName)) return;
		AppiusClaudiusCaecus.logEvent ("part", zone, user.getName (),
				room.getName (), null);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc.makeHashCode (getURL ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#init()
	 */
	public void init () {
		trace ("(" + this.getClass ().toString () + " in " + myName
				+ ")");

		if ("$Eden".equals (myName)) {
			Connection con = null;
			PreparedStatement st = null;
			try {
				con = AppiusConfig.getDatabaseConnection ();
				st = con
				.prepareStatement ("UPDATE zones SET priority=2 WHERE priority=-2");
				st.executeUpdate ();
			} catch (final SQLException e1) {
				AppiusClaudiusCaecus.reportBug (e1);
				return;
			} finally {
				if (null != st) {
					try {
						st.close ();
					} catch (final SQLException e) { /* No Op */
					}
				}
				if (null != con) {
					try {
						con.close ();
					} catch (final SQLException e) { /* No Op */
					}
				}

			}
		}
	}

	/**
	 * Check over Zone spawn
	 */
	private void isItGood () {
		final Room tootSquare27 = roomsByID.get (27);
		if (null == tootSquare27)
			throw AppiusClaudiusCaecus
			.fatalBug ("tootSquare has gone away (by ID)");
		AppiusClaudiusCaecus.blather ("tootSquare vars count = "
				+ tootSquare27.getVariables ().size ());
		if (null == tootSquare27.getVariable ("s"))
			throw AppiusClaudiusCaecus
			.fatalBug ("TootSquare has no sky");
		if (null == tootSquare27.getVariable ("w"))
			throw AppiusClaudiusCaecus
			.fatalBug ("TootSquare has no weather");
		if (null == tootSquare27.getVariable ("f"))
			throw AppiusClaudiusCaecus
			.fatalBug ("TootSquare has no Flash file");
		if (null == tootSquare27.getVariable ("m"))
			throw AppiusClaudiusCaecus
			.fatalBug ("TootSquare has no music");

		final AbstractRoom tootSquare = roomsByMoniker
		.get ("tootSquare");
		if (null == tootSquare)
			throw AppiusClaudiusCaecus
			.fatalBug ("tootSquare has gone away (by moniker)");

		AppiusClaudiusCaecus.blather ("tootSquare vars count = "
				+ tootSquare.getVariables ().size ());

		if (tootSquare.getVariables ().size () != tootSquare27
				.getVariables ().size ())
			throw AppiusClaudiusCaecus
			.fatalBug ("TootSquare variables differ between getByID and getByMoniker");

		User.isItGood ();

		trace ("And root spoke over the waters, and said: let there be "
				+ getName () + ", and it was good.");

	}

	/**
	 * This is an overriding method.
	 * 
	 * @param in WRITEME
	 * @throws IOException WRITEME
	 * @throws ClassNotFoundException WRITEME
	 */
	@SuppressWarnings ("unchecked")
	public void readExternal (final ObjectInput in)
	throws IOException, ClassNotFoundException {
		badges.clear ();
		lobbies.clear ();
		try {
			final JSONObject badgesIn = new JSONObject (in.readUTF ());
			final Iterator <String> badgeKeys = badgesIn.keys ();
			while (badgeKeys.hasNext ()) {
				final String key = badgeKeys.next ();
				badges.put (key, Room.getByMoniker (badgesIn
						.getString (key), this));
			}
			final JSONObject lobbiesIn = new JSONObject (in.readUTF ());
			final Iterator <String> lobbyKeys = lobbiesIn.keys ();
			while (lobbyKeys.hasNext ()) {
				final String key = lobbyKeys.next ();
				lobbies.add (Room.getByMoniker (lobbiesIn
						.getString (key), this));
			}
		} catch (final JSONException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a JSONException in readExternal", e);
		}
		cullRooms.clear ();
		myEmptyZones.clear ();
	}

	/**
	 * Tell SFS to let them in
	 * 
	 * @param nick WRITEME
	 * @param password WRITEME
	 * @param serverThread WRITEME
	 * @param zoneName WRITEME
	 */
	// protected void permitLogin (final User user, final String nick,
	// final String password,
	// final AppiusClaudiusCaecus serverThread,
	// final String zoneName) {
	// try {
	// trace ("//permitLogin (" + nick + "," + zoneName + ")");
	// logIn (user, nick, password, serverThread);
	// trace ("\\\\permitLogin (" + nick + "," + zoneName + ")");
	// }
	// catch (final AlreadyUsedException e) {
	// trace (nick
	// + " is now here twice. Let's get rid of that evil twin...");
	// xh.kickUser (xh.getZone (zoneName).getUserByName (nick), 0,
	// Messages.getText ("login.duplicate", "en", "US"));
	// AppiusClaudiusCaecus.logEvent ("login.duplicate", zoneName,
	// nick, password, null);
	// }
	// }
	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#remove(AbstractUser)
	 */
	public void remove (final AbstractUser thing) {
		if ( ! (thing instanceof User))
			throw AppiusClaudiusCaecus
			.fatalBug ("Non-local user leaving local Zone? "
					+ thing.toString ());
		final User user = (User) thing;
		if (null != user.getRoom ()) {
			user.getRoom ().part (user);
		}
		usersInZone.remove (user.getUserID ());
	}

	/**
	 * Stop accepting new users
	 * 
	 */
	public void retire () {
		Zone.retiredZones.add (this);
		AppiusClaudiusCaecus.remove (this);
	}

	/**
	 * Send an admin message to \a user in room \a room.
	 * 
	 * @author cnicol, brpocock
	 * @param room Room the user is in.
	 * @param user User to send the message to.
	 * @param message Contents of the message being sent.
	 * @see AppiusClaudiusCaecus#sendAdminMessage(String, boolean)
	 */
	@Deprecated
	public void sendAdminMessage (final AbstractRoom room,
			final User user, final String message) {
		if (null == user || null == user.getServerThread ()) return;
		try {
			user.getServerThread ().sendAdminMessage (message, false);
		} catch (final UserDeadException e) {
			// don't know, don't care.
		}
	}

	/**
	 * send badges to an given user in this zone
	 * 
	 * @param user the user to whom to send badges
	 */
	private void sendBadges (final AbstractUser user) {
		final JSONObject notify = getAllBadges_JSON ();
		try {
			sendSuccessReply ("badgeUpdate", notify, user, user
					.getRoomNumber ());
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in badgesChanged", e);
		}

	}

	/**
	 * Send the user a notification that their password was incorrect
	 * 
	 * @param nick WRITEME
	 * @param channel WRITEME
	 * @param user WRITEME
	 * @param zoneName WRITEME
	 * @param password WRITEME
	 */
	protected void sendBadPassword (final String nick,
			final AppiusClaudiusCaecus channel, final User user,
			final String zoneName, final String password) {
		sendLogKO (channel);
		trace ("Our friend "
				+ nick
				+ " isn't actually here: it's an imposter! (I wanted to hear “"
				+ getApple (channel, user.getPassword ()) + "”)");
		AppiusClaudiusCaecus.logEvent ("login.badPass", zoneName, nick,
				password, null);
	}

	/**
	 * <p>
	 * Sends a buddy notice message to the client
	 * </p>
	 * <code>
		{ from: buddyNotice, status: true,
		  notice: { buddy: name, online: boolean,
		            room: moniker, roomName: title } }
		</code>
	 * 
	 * @param buddyName WRITEME
	 * @param isOnline WRITEME
	 * @param roomMoniker WRITEME
	 * @param roomTitle WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @throws JSONException WRITEME
	 */
	protected void sendBuddyNotice (final String buddyName,
			final boolean isOnline, final String roomMoniker,
			final String roomTitle, final User u, final int room)
	throws JSONException {
		final JSONObject notice = new JSONObject ();
		notice.put ("buddy", buddyName);
		notice.put ("online", isOnline);
		notice.put ("roomMoniker", roomMoniker);
		notice.put ("roomTitle", roomTitle);
		final JSONObject reply = new JSONObject ();
		reply.put ("notice", notice);
		sendSuccessReply ("buddyNotice", reply, u, room);
	}

	/**
	 * Send an earnings notice to the client.
	 * 
	 * @param r room
	 * @param u user
	 * @param msg WRITEME
	 * @throws JSONException WRITEME
	 * @deprecated Use
	 *             {@link org.starhope.appius.user.User#sendEarnings(AbstractRoom, String)}
	 *             instead
	 */
	@Deprecated
	public void sendEarnings (final int r, final User u,
			final String msg) throws JSONException {
		u.sendEarnings (r < 0 ? null : roomsByID.get (r), msg);
	}

	/**
	 * @see AppiusClaudiusCaecus#sendError_RAW
	 * @param xtnName the command reporting an error
	 * @param message the error message
	 * @param channel the server thread to receive the error
	 * @deprecated Use
	 *             {@link org.starhope.appius.game.AppiusClaudiusCaecus#sendError_RAW(String,String)}
	 *             instead
	 */
	@Deprecated
	public void sendError_RAW (final String xtnName,
			final String message, final AppiusClaudiusCaecus channel) {
		channel.sendError_RAW (xtnName, message);
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 20, 2009)
	 * 
	 * @param source The method returning the error message
	 * @param error The error message
	 * @param result The payload, if any. May be altered.
	 * @param u The user to whom to send the success reply
	 * @param room The room in which the user is standing
	 * @throws JSONException WRITEME
	 */
	public void sendErrorReply (final String source,
			final String error, final JSONObject result, final User u,
			final int room) throws JSONException {
		JSONObject results = result;
		if (null == result) {
			results = new JSONObject ();
		}
		results.put ("status", false);
		results.put ("from", source);
		results.put ("err", error);
		final LinkedList <AppiusClaudiusCaecus> recipients = new LinkedList <AppiusClaudiusCaecus> ();
		recipients.add (u.getServerThread ());
		System.err.println ("ERROR: For " + u.getName () + " "
				+ results.toString ());
		sendResponse (results, room, u, recipients);
	}

	/**
	 * Send the bucketfuls of information that we force-feed the client
	 * at login...
	 * 
	 * @param zoneName WRITEME
	 * @param nick WRITEME
	 * @param password WRITEME
	 * @param serverThread WRITEME
	 * @param user WRITEME
	 * @param recipients WRITEME
	 */
	@Deprecated
	protected void sendLoginPacket (final String zoneName,
			final String nick, final String password,
			final AppiusClaudiusCaecus serverThread, final User user,
			final LinkedList <AppiusClaudiusCaecus> recipients) {
		serverThread.sendLoginPacket (zoneName, nick, password);
	}

	/**
	 * TODO: document this method (brpocock, Dec 15, 2009)
	 * 
	 * @param recipient WRITEME
	 * @deprecated Use
	 *             {@link org.starhope.appius.game.AppiusClaudiusCaecus#sendLogKO()}
	 *             instead
	 */
	@Deprecated
	protected void sendLogKO (final AppiusClaudiusCaecus recipient) {
		recipient.sendLogKO ();
	}

	/**
	 * WRITEME
	 * 
	 * @param recipient WRITEME
	 * @param messageText WRITEME
	 * @deprecated Use
	 *             {@link org.starhope.appius.game.AppiusClaudiusCaecus#sendLogKO(String)}
	 *             instead
	 */
	@Deprecated
	protected void sendLogKO (final AppiusClaudiusCaecus recipient,
			final String messageText) {
		recipient.sendLogKO (messageText);
	}

	/**
	 * <p>
	 * Sends an (anonymous) moderator message to the user
	 * </p>
	 * 
	 * @param room The room the user is in
	 * @param user The user to whom to send the message
	 * @param message The moderator message to be sent
	 */
	public void sendModMessage (final AbstractRoom room,
			final User user, final String message) {
		if (null == user) return;
		try {
			user.getServerThread ().sendAdminMessage (message, "",
					"MODERATOR", false);
		} catch (final UserDeadException e) {
			// Mod message, oops.
		}
	}

	/**
	 * Tell the user to bugger off, because they don't exist
	 * 
	 * @param recipients WRITEME
	 * @param nick WRITEME
	 * @param zoneName WRITEME
	 * @param password WRITEME
	 */
	protected void sendNoSuchUser (
			final LinkedList <AppiusClaudiusCaecus> recipients,
			final String nick, final String zoneName,
			final String password) {
		final JSONObject result = new JSONObject ();

		try {
			result.put ("_cmd", "logKO");
			result.put ("err", "login.fail");
			result.put ("msg", Messages.getText ("login.fail", "en",
			"US")); // LANG
			sendResponse (result, -1, null, recipients);
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 17,
			// 2009)
			AppiusClaudiusCaecus.reportBug (e);
		}
		trace ("I don't think anyone named " + nick + " really exists.");
		AppiusClaudiusCaecus.logEvent ("login.noUser", zoneName, nick,
				password, null);
	}

	/**
	 * Sends the user the private message /00p$
	 * 
	 * @param u the user to whom we want to send the Oops message
	 */
	public void sendOops (final User u) {
		u.acceptPrivateMessage (u, "/00p$");
	}

	/**
	 * Replaced with just {@link Zone#sendOops(User)}
	 * 
	 * @param u WRITEME
	 * @param room WRITEME
	 */
	@Deprecated
	public void sendOops (final User u, final AbstractRoom room) {
		sendOops (u);
	}

	/**
	 * TODO: document this method (brpocock, Dec 29, 2009)
	 * 
	 * @param result WRITEME
	 * @param room WRITEME
	 * @param u WRITEME
	 * @param recipient WRITEME
	 * @deprecated use
	 *             {@link AppiusClaudiusCaecus#sendResponse(JSONObject, Integer, AbstractUser)}
	 *             directly
	 */
	@Deprecated
	void sendResponse (final JSONObject result, final Integer room,
			final AbstractUser u, final AppiusClaudiusCaecus recipient) {
		try {
			recipient.sendResponse (result, room, u);
		} catch (final UserDeadException e) {
			// Don't ask, don't care
		}
	}

	/**
	 * Send a JSON “extension response” to a list of recipients.
	 * 
	 * @param result the JSON result object (ready to send)
	 * @param room the room in which the event happened, if any.
	 * @param u the sender of the message
	 * @param recipients the set of recipients
	 * @deprecated use
	 *             {@link AppiusClaudiusCaecus#sendResponse(JSONObject, Integer, AbstractUser)}
	 *             directly
	 */
	@Deprecated
	void sendResponse (final JSONObject result, final Integer room,
			final AbstractUser u,
			final Collection <AppiusClaudiusCaecus> recipients) {
		for (final AppiusClaudiusCaecus recipient : recipients) {
			if (null == recipient) {
				System.out
				.println ("Skipping over null recipient in sendResponse");
			} else {
				try {
					recipient.sendResponse (result, room, u);
				} catch (final UserDeadException e) {
					// whatever
				}
			}
		}
	}

	/**
	 * Send a reply with a success indicator to a list of recipients.
	 * 
	 * @param source The method returning the success message
	 * @param resultIn The payload, if any. May be altered.
	 * @param u The user to whom to send the success reply
	 * @param room The room in which the user is standing
	 * @throws JSONException WRITEME
	 */
	public void sendSuccessReply (final String source,
			final JSONObject resultIn, final AbstractUser u,
			final int room) throws JSONException {
		if ( ! (u instanceof User))
			throw AppiusClaudiusCaecus
			.fatalBug ("Non-local user trying to send success to him: "
					+ u.toString ());
		final AppiusClaudiusCaecus thread = ((User) u)
		.getServerThread ();
		if (null == thread) {
			System.out
			.println ("Suppressing attempt to send to null client");
		} else {
			sendSuccessReply (source, resultIn, u, room, thread);
		}
	}

	/**
	 * TODO: document this method (brpocock, Dec 29, 2009)
	 * 
	 * @param source WRITEME
	 * @param resultIn WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @param recipient WRITEME
	 * @throws JSONException WRITEME
	 * @deprecated Use
	 *             {@link org.starhope.appius.game.AppiusClaudiusCaecus#sendSuccessReply(String,JSONObject,AbstractUser,int)}
	 *             instead
	 */
	@Deprecated
	public void sendSuccessReply (final String source,
			final JSONObject resultIn, final AbstractUser u,
			final int room, final AppiusClaudiusCaecus recipient)
	throws JSONException {
		recipient.sendSuccessReply (source, resultIn, u, room);
	}

	/**
	 * WRITEME: document this method (brpocock, Sep 9, 2009)
	 * 
	 * @param source WRITEME
	 * @param resultIn WRITEME
	 * @param u WRITEME
	 * @param room WRITEME
	 * @param recipients WRITEME
	 * @throws JSONException WRITEME
	 * @deprecated This was just being abused to be compatible with SFS,
	 *             you almost certainly just wanted
	 *             {@link #sendSuccessReply(String, JSONObject, AbstractUser,int)}
	 */
	@Deprecated
	public void sendSuccessReply (final String source,
			final JSONObject resultIn, final AbstractUser u,
			final int room,
			final LinkedList <AppiusClaudiusCaecus> recipients)
	throws JSONException {
		for (final AppiusClaudiusCaecus recipient : recipients) {
			sendSuccessReply (source, resultIn, u, room, recipient);
		}
	}

	/**
	 * Sends the user an asynchronous notification of their user lists'
	 * status. This is normally triggered by a change in the status of
	 * one of the users on these lists. (Note, User Lists are the buddy
	 * list and ignore list.)
	 * 
	 * @param user The user who will receive an update of their user
	 *            lists
	 * @throws JSONException If the data fails to serialize properly
	 */
	void sendUserLists (final AbstractUser user) throws JSONException {
		user.sendUserLists ();
	}

	/**
	 * Send a "from:wardrobe" message to the user
	 * 
	 * @param user WRITEME
	 * @param ignored Ignored parameter. Null is a good answer.
	 * @param room WRITEME
	 * @throws JSONException WRITEME
	 */
	void sendWardrobe (final AbstractUser user, final Object ignored,
			final int room) throws JSONException {
		user.sendWardrobe ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#setAutoJoinRoom(int)
	 */
	public void setAutoJoinRoom (final int id) {
		lobbies.add (getRoom (id));
	}

	/**
	 * WRITEME
	 * 
	 * @param badge WRITEME
	 * @param room WRITEME
	 */
	public void setBadge (final String badge, final AbstractRoom room) {
		badges.put (badge.toLowerCase (Locale.ENGLISH), room);
		badgesChanged ();
	}

	/**
	 * @param server {@link #myServer}
	 */
	public void setMyServer (final String server) {
		myServer = server;
	}

	/**
	 * @param serverPort {@link #myServerPort}
	 */
	public void setMyServerPort (final int serverPort) {
		myServerPort = serverPort;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#spawnNewZone()
	 */
	public String spawnNewZone () {
		trace ("And it came to pass in those days, that the elephants had increased greatly in numbers, and exceeded the Zones allotted to them. And so a new Zone was called for, from the rolls of Zones that might have been.");
		PreparedStatement st = null;
		String zoneName = "";
		try {
			st = AppiusConfig
			.getZonesDatabaseConnection ()
			.prepareStatement (
			"SELECT * FROM zones WHERE serverName=? AND priority=2 LIMIT 1");
			st.setString (1, AppiusConfig.getServerName ());
			if (st.execute ()) {
				final ResultSet zones = st.getResultSet ();
				if (zones.next ()) {
					zoneName = zones.getString ("zoneName");
					spawnZone (zoneName, zones.getString ("image"));
				}
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) {
					AppiusClaudiusCaecus.reportBug (e);
				}
			}
		}
		if ("".equals (zoneName)) {
			spawnZone (genZoneName ());
		}
		return zoneName;
	}

	/**
	 * Create a new, empty zone, and attach the default properties to
	 * it. The zone's image will default to the one for the current
	 * Zone.
	 * 
	 * @param zoneName the name of the new zone
	 */
	public void spawnZone (final String zoneName) {
		spawnZone (zoneName, getRoomByName ("$Eaves").getVariable (
		"zoneImage"));
	}

	/**
	 * Create a new, empty zone, and attach the default properties to
	 * it.
	 * 
	 * @param zoneName the name of the new zone
	 * @param image the background nugget icon
	 */
	public void spawnZone (final String zoneName, final String image) {
		trace ("...so the Systems Programmer waved his hand across the face of the keyboard, and said: “Let there be "
				+ zoneName + "”");
		final Zone zone = new Zone (zoneName);

		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("UPDATE zones SET priority=-2 WHERE zoneName=?");
			st.setString (1, zoneName);
			st.executeUpdate ();
		} catch (final SQLException e1) {
			AppiusClaudiusCaecus.reportBug (e1);
			return;
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}

		for (final AbstractRoom theoreticalRoom : org.starhope.appius.game.Room
				.getAllRooms ()) {
			AbstractRoom actualRoom;
			// trace (" Creating " + zoneName + "#" +
			// gameRoom.getMoniker ());
			try {
				actualRoom = Room.create (
						theoreticalRoom.getMoniker (), zone, true);
			} catch (final NotFoundException e) {
				throw AppiusClaudiusCaecus.fatalBug (e);
			}
			actualRoom.setRoomVars ();

			if (actualRoom.getMoniker ().equals ("tootSquare")) {
				zone.setAutoJoinRoom (actualRoom.getID ());
			}
		}

		trace ("Setting up Zap Attack game");
		final ZapAttack zapAttack = new ZapAttack (zone);
		zapAttack.noOp (); // avert compiler warning

		trace ("Setting up Shaddow Falls");
		final ShaddowFalls shaddowFalls = new ShaddowFalls (zone);
		shaddowFalls.noOp (); // avert compiler warning

		trace ("Sneaky people have been known to hang in the eaves and snoop upon the presumably-innocent");
		final Room eaves = Room.create ("$Eaves", zone);
		eaves.setVariable ("zoneImage", image);
		// trace
		// ("It was ordained by Lapo that there must be Limbo, where unbaptised Catholic babies are kept in Hell beside the fires");
		trace ("nowhere");
		final AbstractRoom nowhere = Room.create ("nowhere", zone);
		nowhere.setLimbo (true);

		zone.isItGood ();

		zone.activate ();
	}

	/**
	 * TODO: document this method (brpocock, Oct 28, 2009)
	 * 
	 * @param user The user who has acted
	 * @param room The room in which s/he has acted
	 * @param verb The action s/he has taken
	 * @param note Note(s) about the action
	 */
	public void tellEaves (final AbstractUser user,
			final AbstractRoom room, final String verb,
			final String note) {
		final AbstractRoom localEaves = getRoomByName ("$Eaves");
		if (null == localEaves) return;
		final Collection <AbstractUser> eavesdroppers = localEaves
		.getAllUsers ();
		if (eavesdroppers.size () > 0) {
			localEaves.sendPublicMessage (user,
					(null == user ? "(someone)" : user
							.getAvatarLabel ())
							+ (char) 0x1f
							+ (null == room ? "(lost)" : room
									.getName ())
									+ (char) 0x1f
									+ verb
									+ (char) 0x1f + note + (char) 0x20);
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.AbstractZone#trace(java.lang.String)
	 */
	public void trace (final String string) {
		System.err.println (new java.util.Date ().toString () + " ["
				+ "Zone “" + myName + "”] " + string);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.via.ViaAppia#writeExternal(java.io.ObjectOutput)
	 */
	public void writeExternal (final ObjectOutput out)
	throws IOException {
		// TODO Auto-generated method stub (brpocock, Dec 9, 2009)
	}

}
