/**
 * <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.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 org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.game.inventory.ClothingItem;
import org.starhope.appius.sql.SQLPeerDatum;
import org.starhope.appius.sys.op.OpCommands;
import org.starhope.appius.types.AbstractZone;
import org.starhope.appius.types.HasVariables;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.AvatarClass;
import org.starhope.appius.user.User;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;

/**
 * WRITEME: The documentation for this type (Room) is incomplete.
 * (brpocock, Aug 18, 2009)
 * 
 * @author brpocock
 */
public class Room extends SQLPeerDatum implements
Comparable <AbstractRoom>, HasVariables, AbstractRoom {

	/**
	 * WRITEME
	 */
	private static String fairgroundsImage = null;

	/**
	 * WRITEME
	 */
	private static String fairgroundsLabel = null;

	/**
	 * WRITEME
	 */
	private static String fairgroundsWarp = null;

	/**
	 * TODO: document this field (brpocock, Nov 13, 2009) safeRoomNum
	 * (Room)
	 */
	private static int safeRoomNum = 0x100;

	/**
	 * TODO: document this field (brpocock, Oct 5, 2009)
	 * serialVersionUID (long)
	 */
	private static final long serialVersionUID = 777843822192305721L;

	/**
	 * TODO: document this method (brpocock, Oct 19, 2009)
	 * <p>
	 * Creates only temporary/anonymous rooms without referring to the
	 * database.
	 * </p>
	 * 
	 * @see Room#create(String, AbstractZone, boolean)
	 * @param roomName the moniker of the room
	 * @param zone the zone in which the room is to be created
	 * @return WRITEME
	 */
	public static Room create (final String roomName,
			final AbstractZone zone) {
		System.err.println ("Room.create (" + roomName + ","
				+ zone.getName () + ") *anonymous room");
		final Room newRoom = new Room (zone);
		newRoom.setMoniker (roomName);
		newRoom.myID = Room.getNextID ();
		zone.add (newRoom);
		return newRoom;
	}

	/**
	 * @param roomName WRITEME
	 * @param abstractZone WRITEME
	 * @param mustExist If true, a named room from the database. If
	 *            false, an anonymous/temporary room.
	 * @return WRITEME
	 * @throws NotFoundException Room moniker not found
	 */
	public static Room create (final String roomName,
			final AbstractZone abstractZone, final boolean mustExist)
	throws NotFoundException {
		if ( !mustExist) return Room.create (roomName, abstractZone);
		// System.err.println ("Room.create (" + roomName + ","
		// + abstractZone.getName () + ") *named room");
		final Room newRoom = new Room (roomName, abstractZone);
		return newRoom;
	}

	/**
	 * @return an array of all rooms in the game
	 */
	public static AbstractRoom [] getAllRooms () {
		final Vector <Room> everywhere = new Vector <Room> ();

		PreparedStatement st = null;
		java.sql.Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con.prepareStatement ("SELECT * FROM roomList");
			if (st.execute ()) {
				final ResultSet rs = st.getResultSet ();
				while (rs.next ()) {
					everywhere.add (new Room (rs, null));
				}
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} 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 */
				}
			}

		}
		final AbstractRoom [] everywhereArray = new AbstractRoom [everywhere
		                                                          .size ()];
		final Iterator <Room> copier = everywhere.iterator ();
		int i = 0;
		while (copier.hasNext ()) {
			everywhereArray [i++ ] = copier.next ();
		}
		return everywhereArray;
	}

	/**
	 * @param moniker the moniker value for this room
	 * @param zone the zone containing this room
	 * @return the room object
	 */
	public static Room getByMoniker (final String moniker,
			final Zone zone) {
		try {
			return new Room (moniker, zone);
		} catch (final NotFoundException e) {
			return null;
		}
	}

	/**
	 * TODO: document this method (brpocock, Oct 19, 2009)
	 * 
	 * @return WRITEME
	 */
	private synchronized static int getNextID () {
		return Room.safeRoomNum++ ;
	}

	/**
	 * The base name of the room's SWF file. This is going to be in a
	 * given resource folder for rooms.
	 */
	private String filename;
	/**
	 * The Game Event in this room, or (usually) null
	 */
	private GameEvent gameEvent = null;

	/**
	 * <p>
	 * The owner of a room; usually null for public rooms, or non-null
	 * for users' houses.
	 * </p>
	 * <p>
	 * The special room variable homeOwner reflects this.
	 * </p>
	 */
	private AbstractUser homeOwner = null;
	/**
	 * Determine whether this room is a limbo room
	 * 
	 * @see AbstractRoom#isLimbo() discussion of Limbo on
	 *      AbstractRoom.isLimbo ()
	 */
	private transient boolean iAmInLimbo = false;
	/**
	 * The moniker / unique identifier for this room.
	 */
	private String moniker;

	/**
	 * in seconds
	 */
	private int movieTime = -1;
	/**
	 * TODO: document this field (brpocock, Nov 13, 2009) movieURL
	 * (Room)
	 */
	private String movieURL = null;

	/**
	 * The background music for the room
	 */
	private String music = "";

	/**
	 * TODO: document this field (brpocock, Nov 13, 2009) myID (Room)
	 */
	private int myID;
	/**
	 * TODO: document this field (brpocock, Nov 13, 2009) nowPlaying
	 * (Room)
	 */
	private String nowPlaying = null;

	/**
	 * Non-Users who are present to pay attention to things happening in
	 * the room
	 */
	private final ConcurrentSkipListSet <RoomListener> observers = new ConcurrentSkipListSet <RoomListener> ();
	/**
	 * The SWF file to be displayed as a weather overlay.
	 */
	private String overlay;

	/**
	 * WRITEME: document this field (brpocock, Jan 14, 2010) roomIndex
	 * (Room)
	 */
	private int roomIndex;

	/**
	 * WRITEME
	 */
	private final ConcurrentHashMap <String, String> roomVariables = new ConcurrentHashMap <String, String> ();

	/**
	 * The SWF file to be displayed as a sky background.
	 */
	private String sky;

	/**
	 * Whether the sky is visible in this room.
	 */
	private boolean skyVisible;

	/**
	 * WRITEME
	 */
	private String title;

	/**
	 * The list of user ID's of all users in this room. Using user ID's
	 * instead of user objects saves from having fallow references.
	 */
	private final ConcurrentSkipListSet <Integer> userList = new ConcurrentSkipListSet <Integer> ();

	/**
	 * The zone in which this room exists
	 */
	private transient AbstractZone zone;

	/**
	 * @param homeZone the zone in which the room is created.
	 */
	public Room (final AbstractZone homeZone) {
		iAmInLimbo = false;
		zone = homeZone;
		roomIndex = -1;
		homeOwner = null;
	}

	/**
	 * Instantiate a room based upon the theoretical room dataset from
	 * the database and the Zone into which it should be instantiated.
	 * 
	 * @param rs The theoretical room dataset
	 * @param newZone The zone into which the room should be
	 *            instantiated
	 * @throws SQLException if the room dataset can't be interpreted
	 */
	private Room (final ResultSet rs, final AbstractZone newZone)
	throws SQLException {
		iAmInLimbo = false;
		zone = newZone;
		this.set (rs);
	}

	/**
	 * @param newRoomMoniker The moniker of the room to be instantiated
	 * @param newRoomZone WRITEME
	 * @throws NotFoundException if there is no room in the database
	 *         with the given moniker
	 */
	public Room (final String newRoomMoniker,
			final AbstractZone newRoomZone)
	throws NotFoundException {
		// System.err.println ("constructing named room from moniker");

		iAmInLimbo = false;
		zone = newRoomZone;

		PreparedStatement select = null;
		Connection con = null;

		/*
		 * Get database record
		 */

		try {
			con = AppiusConfig.getDatabaseConnection ();
			select = con
			.prepareStatement ("SELECT * FROM roomList WHERE moniker=?");
			select.setString (1, newRoomMoniker);
		} catch (final SQLException e) {
			if (null != select) {
				try {
					select.close ();
				} catch (final SQLException e1) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e1) { /* No Op */
				}
			}
			throw AppiusClaudiusCaecus.fatalBug (e);
		}

		/*
		 * Parse database record
		 */

		try {
			if (select.execute ()) {
				final ResultSet resultSet = select.getResultSet ();
				if (resultSet.next ()) {
					this.set (resultSet);
				} else throw new NotFoundException (
						"Can't find any room with moniker "
						+ newRoomMoniker);

			} else throw new NotFoundException (
					"Can't find any room with moniker "
					+ newRoomMoniker);
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			try {
				select.close ();
			} catch (final SQLException e) { /* No Op */
			}

			try {
				con.close ();
			} catch (final SQLException e) { /* No Op */
			}

		}

		zone.add (this);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#add(org.starhope.appius.game.GameEvent)
	 */
	public void add (final GameEvent game) {
		gameEvent = game;
	}

	/**
	 * @see Comparable#compareTo(Object)
	 */
	public int compareTo (final AbstractRoom other) {
		return other.getDebugName ().compareTo (getDebugName ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#contains(org.starhope.appius.user.AbstractUser)
	 */
	public boolean contains (final AbstractUser thing) {
		if (thing instanceof User)
			return userList.contains ( ((User) thing).getUserID ());
		return false;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#deleteVariable(java.lang.String)
	 */
	public void deleteVariable (final String string) {
		this.setVariable (string, "");
		if ( !"f".equals (string)) {
			roomVariables.remove (string);
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#destroySelf()
	 */
	public void destroySelf () {
		// TODO?
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see Object#equals(Object)
	 * @param other the other room to which this one will be compared
	 * @return true, if the two objects represent the same room instance
	 *         (in the same Zone, &c.)
	 */
	public boolean equals (final AbstractRoom other) {
		return other.getDebugName ().equals (getDebugName ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#equals(java.lang.Object)
	 */
	@Override
	public boolean equals (final Object other) {
		if (other instanceof Room)
			return this.equals ((AbstractRoom) other);
		return false;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#flush()
	 */
	@Override
	public void flush () {
		// NO OP
		// AppiusClaudiusCaecus.reportBug
		// ("I don't save rooms, just yet.");
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#fromJSON(org.json.JSONObject)
	 */
	@SuppressWarnings ( { "cast", "unchecked" })
	public void fromJSON (final JSONObject jso) throws JSONException {
		if (jso.getLong ("serialVersionUID") != Room.serialVersionUID)
			throw new JSONException ("Mismatched serialVersionUID");
		myID = jso.getInt ("id");
		if (Room.safeRoomNum == myID) {
			++Room.safeRoomNum;
		}
		filename = jso.getString ("filename");
		iAmInLimbo = jso.getBoolean ("iAmInLimbo");
		moniker = jso.getString ("moniker");
		if (jso.has ("movieTime")) {
			movieTime = jso.getInt ("movieTime");
		}
		if (jso.has ("movieURL")) {
			movieURL = jso.getString ("movieURL");
		}
		music = jso.getString ("music");
		nowPlaying = jso.getString ("nowPlaying");
		overlay = jso.getString ("overlay");
		final String f = roomVariables.get ("f");
		roomVariables.clear ();
		roomVariables.putAll ((HashMap <String, String>) jso
				.getJSONObject ("roomVars").getData ());
		if (null == roomVariables.get ("f")) {
			roomVariables.put ("f", f);
		}
		sky = jso.getString ("sky");
		skyVisible = jso.getBoolean ("skyVisible");
		title = jso.getString ("title");
		userList.clear ();
		zone = AppiusClaudiusCaecus.getZone (jso.getString ("zone"));
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getAllListeners()
	 */
	public Set <RoomListener> getAllListeners () {
		final HashSet <RoomListener> listeners = new HashSet <RoomListener> ();
		listeners.addAll (getAllUsers ());
		if (null != gameEvent) {
			listeners.add (gameEvent);
		}
		return listeners;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getAllUsers()
	 */
	public Collection <AbstractUser> getAllUsers () {
		final HashSet <AbstractUser> users = new HashSet <AbstractUser> ();
		for (final Integer id : userList) {
			users.add (User.getByID (id));
		}
		return users;
	}

	/**
	 *Load arbitrary room variable values from the database
	 * 
	 * @return the set of variables for this room
	 */
	private HashMap <String, String> getArbitraryVars () {
		Connection con = null;
		PreparedStatement st = null;
		final HashMap <String, String> vars = new HashMap <String, String> ();
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM roomVars WHERE room=?");
			st.setString (1, moniker);
			rs = st.executeQuery ();
			while (rs.next ()) {
				vars.put (rs.getString ("keyName"), rs
						.getString ("value"));
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
		return vars;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#getCacheUniqueID()
	 */
	@Override
	protected String getCacheUniqueID () {
		return moniker;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getDebugName()
	 */
	public String getDebugName () {
		if (null == zone)
			return "Abstract Room “" + getMoniker () + "”";
		return "Room “" + getMoniker () + "” in Zone “"
		+ zone.getName () + "”";
	}

	/**
	 * TODO: document this method (brpocock, Nov 24, 2009)
	 * 
	 * @return All things in the room
	 */
	private Set <AbstractUser> getEverythingInRoom () {
		final HashSet <AbstractUser> stuff = new HashSet <AbstractUser> ();
		for (final RoomListener thing : getAllListeners ()) {
			if (thing instanceof AbstractUser) {
				stuff.add ((AbstractUser) thing);
			}

		}
		return stuff;
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @throws SQLException WRITEME
	 */
	private void getFairground () throws SQLException {

		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con.prepareStatement ("SELECT * FROM fairgrounds");
			final ResultSet rs = st.executeQuery ();
			rs.next ();
			Room.fairgroundsImage = rs.getString ("image");
			Room.fairgroundsLabel = rs.getString ("title");
			Room.fairgroundsWarp = rs.getString ("filename");
			rs.close ();
		} catch (final SQLException e) {
			throw e;
		} 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 */
				}
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getFilename()
	 */
	public String getFilename () {
		// default getter (brpocock, Aug 18, 2009)
		return filename;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getGameEvent()
	 */
	public GameEvent getGameEvent () {
		return gameEvent;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getId()
	 */
	@Deprecated
	public int getId () {
		return getID ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getID()
	 */
	public int getID () {
		return myID;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getListeners()
	 */
	public Collection <RoomListener> getListeners () {
		final HashSet <RoomListener> listeners = new HashSet <RoomListener> ();
		listeners.addAll (listeners);
		for (final Integer uid : userList) {
			listeners.add (User.getByID (uid));
		}
		return listeners;
	}

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

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getMoniker()
	 */
	public String getMoniker () {
		return moniker;
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @throws SQLException WRITEME
	 */
	private void getMovie () throws SQLException {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT title, filename, duration FROM movieListings WHERE sequence=0");
			rs = st.executeQuery ();
			rs.next ();
			nowPlaying = rs.getString ("title");
			movieURL = rs.getString ("filename");
			movieTime = rs.getInt ("duration");
			rs.close ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			throw e;
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getMusic()
	 */
	public String getMusic () {
		return music;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getName()
	 */
	public String getName () {
		return getMoniker ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getOverlay()
	 */
	public String getOverlay () {
		// default getter (brpocock, Aug 18, 2009)
		return overlay;
	}

	/**
	 * @see org.starhope.appius.game.AbstractRoom#getOwner()
	 */
	public AbstractUser getOwner () {
		return homeOwner;
	}

	/**
	 * @see org.starhope.appius.game.AbstractRoom#getRoomIndex()
	 */
	public int getRoomIndex () {
		return roomIndex;
	}

	/**
	 * get the JSON sequence to be passed to an user upon successfully
	 * joining a room (the “joinOK” message)
	 * 
	 * @param user The user to whom the XML will be sent
	 * @return a string suitable for return to the user
	 */
	public JSONObject getRoomJoinJSON (final AbstractUser user) {
		final JSONObject reply = new JSONObject ();
		try {
			reply.put ("from", "roomJoin");
			reply.put ("status", "true");
			reply.put ("roomNumber", getID ());
			reply.put ("moniker", getMoniker ());
			reply.put ("limbo", isLimbo ());

			final JSONObject users = new JSONObject ();
			if (isLimbo ()) {
				users.put (String.valueOf (user.getUserID ()), user
						.getPublicInfo ());
			} else {
				for (final AbstractUser who : getEverythingInRoom ()) {
					users.put (String.valueOf (who.getUserID ()), who
							.getPublicInfo ());
				}
			}
			reply.put ("users", users);

			final JSONObject vars = new JSONObject ();
			for (final Entry <String, String> var : getVariables ()
					.entrySet ()) {
				vars.put (var.getKey (), var.getValue ());
			}
			reply.put ("vars", vars);
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Jan 21, 2010)
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in getRoomJoinJSON", e);
		}

		return reply;
	}

	/**
	 * get the Smart Fox Server XML sequence to be passed to an user
	 * upon successfully joining a room (the “joinOK” message)
	 * 
	 * @param user The user to whom the XML will be sent
	 * @return a string suitable for return to the user
	 */
	private String getRoomJoinSFSXML (final User user) {
		final StringBuffer reply = new StringBuffer (
				"<msg t='sys'><body action='joinOK' r='" + getID ()
				+ "'><pid id='0'/>");

		if (isLimbo ()) {
			reply.append ("<vars></vars><uLs r='" + getID () + "'>");
			reply.append (user.toSFSXML ());
			reply.append ("</uLs>");
		} else {
			// Room Variables
			reply.append ("<vars>");
			for (final java.util.Map.Entry <String, String> var : getVariables ()
					.entrySet ()) {
				reply.append ("<var n='");
				reply.append (var.getKey ());
				reply.append ("' t='s'><![CDATA[");
				reply.append (var.getValue ());
				reply.append ("]]></var>");
			}
			reply.append ("</vars>");

			// User List with user variables
			reply.append ("<uLs r='" + getID () + "'>");
			for (final AbstractUser u : getEverythingInRoom ()) {
				reply.append (u.toSFSXML ());
			}
			reply.append ("</uLs>");

		}

		reply.append ("</body></msg>");
		return reply.toString ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getSky()
	 */
	public String getSky () {
		// default getter (brpocock, Aug 18, 2009)
		return sky;
	}

	/**
	 * Obtain Super Toot Bots for a room. FIXME: This doesn't belong
	 * here.
	 * 
	 * @param padding WRITEME
	 * @return { from: bots, status: true, bots: { "botname": name,
	 *         avatar: { avatar: "toot.swf", clothes: { 0: { color:
	 *         patcol, id: patitem }, 1: { id: clothes }, ... }, colors:
	 *         { 0: base, 1: extra } }, speech : { 0: "blah" ,... } , x:
	 *         startx, y: starty, walkFreq: NN, talkFreq: NNN, facing:
	 *         S} }
	 * @throws SQLException WRITEME
	 * @throws JSONException WRITEME
	 * @deprecated Does not belong in Room ... but will be removed when
	 *             NPC's work and Super Toot Bots can be removed from
	 *             the codebase ... don't count on this being around
	 *             forever, but it won't disappear as long as Super Toot
	 *             Bots exist.
	 */
	@Deprecated
	public JSONObject getSuperTootBots (final int padding)
	throws SQLException, JSONException {

		Connection con = null;
		PreparedStatement getBots = null;
		PreparedStatement getDetails = null;
		PreparedStatement getSpeech = null;
		ResultSet rs = null;

		final JSONObject bots = new JSONObject ();
		try {

			final String myMoniker = getMoniker ();
			if (null == myMoniker || "nowhere".equals (myMoniker)
					|| myMoniker.length () == 0) return bots;

			con = AppiusConfig.getDatabaseConnection ();
			getBots = con
			.prepareStatement ("SELECT * FROM tootBotSchedule WHERE ( (fromTime <= TIME(NOW()) ) AND (toTime >= TIME(NOW()) ) ) AND room=?");

			getDetails =
				con
				.prepareStatement ("SELECT * FROM superTootBots WHERE botName=?");

			getSpeech = con
			.prepareStatement ("SELECT text FROM nightmare_bot_speech WHERE botName=?");

			getBots.setString (1, myMoniker);
			if (getBots.execute ()) {
				final ResultSet botGot = getBots.getResultSet ();
				while (botGot.next ()) {

					getDetails.setString (1, botGot
							.getString ("botName"));
					if (getDetails.execute ()) {

						rs = getDetails.getResultSet ();
						if (rs.next ()) {
							final JSONObject bot = new JSONObject ();
							final JSONObject clothes = new JSONObject ();
							final JSONObject colors = new JSONObject ();

							superbot_fetchPattern (rs, clothes);
							superbot_fetchClothing (rs, clothes);

							final JSONObject speech = superbot_fetchSpeech (
									getSpeech, rs);

							final JSONObject avatar = new JSONObject ();
							avatar.put ("clothes", clothes);

							colors.put ("1", rs.getInt ("extraColor"));
							colors.put ("0", rs.getInt ("baseColor"));
							avatar.put ("colors", colors);
							avatar.put ("avatar", AvatarClass.getByID (
									rs.getInt ("avatarClass"))
									.getFilename ());
							superbot_fetchBasicInfo (rs, botGot, bot,
									speech, avatar);
							bots.put (rs.getString ("botName"), bot);
						}
					}
				}
			}
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != getBots) {
				try {
					getBots.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != getSpeech) {
				try {
					getSpeech.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != getDetails) {
				try {
					getDetails.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}

		return bots;
	}

	/**
	 * @return the JSON structure describing any SuperTootBoys presently
	 *         in the room
	 * @deprecated Needs to be moved someplace more appropriate ... not
	 *             yet determined ...
	 */
	@Deprecated
	private JSONObject getSuperTootBots_JSON () {
		final JSONObject jso = new JSONObject ();
		try {
			jso.put ("from", "bots");
			jso.put ("bots", getSuperTootBots (0));
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Can't set up Super Toot Bots schedule", e);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (
					"Can't read Super Toot Bots from database", e);
		}
		return jso;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getTitle()
	 */
	public String getTitle () {
		if (null == title || title.length () == 0) return "Tootsville";
		return title;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getUserCount()
	 */
	public int getUserCount () {
		if (isLimbo ()) return 1;
		return userList.size ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getVariable(java.lang.String)
	 */
	public String getVariable (final String string) {
		final String var = roomVariables.get (string);
		if (null == var) return "";
		return var;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getVariables()
	 */
	public HashMap <String, String> getVariables () {
		final HashMap <String, String> ret = new HashMap <String, String> ();
		ret.putAll (roomVariables);
		return ret;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#getZone()
	 */
	public AbstractZone getZone () {
		return zone;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc.makeHashCode (getCacheUniqueID ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#isLimbo()
	 */
	public boolean isLimbo () {
		return iAmInLimbo;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#isSkyVisible()
	 */
	public boolean isSkyVisible () {
		return skyVisible;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#join(org.starhope.appius.game.RoomListener)
	 */
	public void join (final RoomListener thing) {
		if (null == thing) return;

		if ( ! (thing instanceof User)) {
			AppiusClaudiusCaecus
			.reportBug ("Room listener who isn't a User tried to join "
					+ getDebugName ()
					+ "; "
					+ thing.toString ());
			return;
		}

		final User user = (User) thing;
		validateUserList ();

		user.getServerThread ().tattle (
				"User joining room "
				+ getMoniker ()
				+ " from room "
				+ (null == user.getRoom () ? "(null)" : user
						.getRoom ().getMoniker ()));
		zone.tellEaves (user, this, "join", "");

		userList.add (user.getUserID ());
		final AbstractRoom oldRoom = user.changeRoom (this);
		if (null != oldRoom && !oldRoom.getMoniker ().equals (moniker)) {
			oldRoom.part (user);
		}

		user.setRoom (this);
		try {
			user.getServerThread ().sendRawMessage (
					getRoomJoinSFSXML (user), false);
			user.getServerThread ().sendResponse (
					getSuperTootBots_JSON (), getID (), user);
		} catch (final UserDeadException e1) {
			// Nifty, he died. This is gonna go away quickly.
			// Let's not even let anybody else hear about this.
			return;
		}

		final JSONObject serverTime = new JSONObject ();
		try {
			serverTime.put ("serverTime", System.currentTimeMillis ());
			user.acceptSuccessReply ("serverTime", serverTime, this);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}

		AppiusClaudiusCaecus.logEvent ("join", zone.getName (), user
				.getName (), getName (), null);

		user.getServerThread ().tattle (
				"User is now in room "
				+ (null == user.getRoom () ? "(null)" : user
						.getRoom ().getMoniker ()));

		if (null == oldRoom || !moniker.equals (oldRoom.getMoniker ())) {
			for (final RoomListener observer : getAllListeners ()) {
				if (!thing.equals (observer)) {
					observer.acceptObjectJoinRoom (this, thing);
				}
			}
		}

		if (null != gameEvent) {
			gameEvent.acceptObjectJoinRoom (this, user);
		}

	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#part(RoomListener)
	 */
	public void part (final RoomListener thing) {
		observers.remove (thing);
		if (thing instanceof AbstractUser) {
			zone.tellEaves ((AbstractUser) thing, this, "part", "");
			userList.remove ( ((AbstractUser) thing).getUserID ());
		}
		for (final RoomListener observer : getAllListeners ()) {
			observer.acceptObjectPartRoom (this, thing);
		}

	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#sendGameAction(org.starhope.appius.user.User,
	 *      org.json.JSONObject)
	 */
	public void sendGameAction (
			final org.starhope.appius.user.User from,
			final JSONObject data) throws JSONException {
		data.put ("fromUser", data.getString ("from"));
		data.put ("from", "gameAction");
		for (final Integer uid : userList) {
			final AbstractUser u = User.getByID (uid);
			u.acceptGameAction (from, data);
		}
		if (null != gameEvent) {
			gameEvent.acceptGameAction (from, data);
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#sendPublicMessage(org.starhope.appius.user.AbstractUser,
	 *      java.lang.String)
	 */
	public void sendPublicMessage (final AbstractUser from,
			final String speech) {
		for (final Integer uid : userList) {
			final AbstractUser u = User.getByID (uid);
			u.acceptPublicMessage (from, speech);
		}
		if (null != gameEvent) {
			gameEvent.acceptPublicMessage (from, speech);
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#set(java.sql.ResultSet)
	 */
	@Override
	protected void set (final ResultSet rs) throws SQLException {
		setFilename (rs.getString ("filename"));
		setMoniker (rs.getString ("moniker"));
		setSky (rs.getString ("sky"));
		setOverlay (rs.getString ("overlay"));
		setSkyVisible (rs.getString ("skyVisible").equals ("Y"));
		setMusic (rs.getString ("music"));
		setTitle (rs.getString ("title"));
		myID = rs.getInt ("ID");
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setFilename(java.lang.String)
	 */
	public void setFilename (final String filename1) {
		// default setter (brpocock, Aug 18, 2009)
		filename = filename1;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setLimbo(boolean)
	 */
	public void setLimbo (final boolean b) {
		iAmInLimbo = b;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setMoniker(java.lang.String)
	 */
	public void setMoniker (final String moniker1) {
		moniker = moniker1;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setMusic(java.lang.String)
	 */
	public void setMusic (final String music1) {
		music = music1;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setOverlay(java.lang.String)
	 */
	public void setOverlay (final String overlay1) {
		overlay = overlay1;
	}


	/**
	 * 
	 * @param newHomeOwner the new owner of the home containing this
	 *            room. May be null.
	 */
	public void setOwner (final AbstractUser newHomeOwner) {
		homeOwner = newHomeOwner;
		if (null == homeOwner) {
			deleteVariable ("homeOwner");
		} else {
			setVariable ("homeOwner", newHomeOwner.getAvatarLabel ()
					.toLowerCase (Locale.ENGLISH));
		}
	}

	/**
	 * 
	 * @param newRoomIndex the new room index within the user's house
	 */
	public void setRoomIndex (final int newRoomIndex) {
		roomIndex = newRoomIndex;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setRoomVars()
	 */
	public void setRoomVars () {
		this.setVariable ("s", sky);
		this.setVariable ("w", overlay);
		this.setVariable ("f", filename);
		this.setVariable ("m", getMusic ());

		if (moniker.equals ("tootSquareWest")) {
			try {
				getMovie ();
			} catch (final SQLException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
			this.setVariable ("nowPlaying", nowPlaying);
			this.setVariable ("movieDuration", String
					.valueOf (movieTime));
			try {
				getFairground ();
			} catch (final SQLException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
			this
			.setVariable ("fairgroundsLabel",
					Room.fairgroundsLabel);
			this.setVariable ("fairgroundsWarp", Room.fairgroundsWarp);
			this
			.setVariable ("fairgroundsImage",
					Room.fairgroundsImage);
		}

		if (moniker.equals ("tootTheater")) {
			try {
				getMovie ();
			} catch (final SQLException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
			this.setVariable ("movie", movieURL);
			this.setVariable ("movieDuration", String
					.valueOf (movieTime));
		}

		if (moniker.equals ("bigTootoonaVolleyball")
				|| moniker.equals ("soccerField")) {
			for (final String var : new String [] { "XPos", "YPos",
					"XVel", "YVel" }) {
				this.setVariable (var, "0");
			}
		}

		for (final Entry <String, String> el : getArbitraryVars ()
				.entrySet ()) {
			this.setVariable (el.getKey (), el.getValue ());
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setSky(java.lang.String)
	 */
	public void setSky (final String sky1) {
		sky = sky1;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setSkyVisible(boolean)
	 */
	public void setSkyVisible (final boolean skyVisible1) {
		// default setter (brpocock, Aug 18, 2009)
		skyVisible = skyVisible1;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setTitle(java.lang.String)
	 */
	public void setTitle (final String newTitle) {
		title = newTitle;
		changed ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setVariable(java.util.Map.Entry)
	 */
	public void setVariable (final Entry <String, String> var) {
		this.setVariable (var.getKey (), var.getValue ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setVariable(java.lang.String,
	 *      java.lang.String)
	 */
	public void setVariable (final String varName, final String varValue) {
		if (null == varName) {
			AppiusClaudiusCaecus.reportBug (
					"Received a null in setVariable",
					new NullPointerException ("null=" + varValue));
			return;
		}
		getZone ().trace (
				"Setting room variable “" + varName + "” to “"
				+ varValue + "”");
		final String varV = null == varValue ? "" : varValue;
		if (varV.equals (roomVariables.get (varName))) return;
		if ("".equals (varV)) {
			if ("f".equals (varName)) {
				OpCommands
				.op_wallops (new String [] {
						"Tried to reset Flash file for ",
						getMoniker () }, User.getByID (1), this);
				return;
			}
		}
		roomVariables.put (varName, varV);
		if ( !isLimbo ()) {
			for (final Integer uid : userList) {
				final AbstractUser u = User.getByID (uid);
				try {
					u.getServerThread ().sendRoomVar (getID (),
							varName, varV);
				} catch (final UserDeadException e) {
					// not so much
				}
			}
		} else {
			System.err
			.println (" Suppressing room var updates in limbo");
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#setVariables(java.util.Map)
	 */
	public void setVariables (final Map <String, String> map) {
		for (final Entry <String, String> var : map.entrySet ()) {
			this.setVariable (var);
		}
	}

	/**
	 * TODO: document this method (brpocock, Dec 29, 2009)
	 * 
	 * @param rs WRITEME
	 * @param botGot WRITEME
	 * @param bot WRITEME
	 * @param speech WRITEME
	 * @param avatar WRITEME
	 * @throws JSONException WRITEME
	 * @throws SQLException WRITEME
	 */
	private void superbot_fetchBasicInfo (final ResultSet rs,
			final ResultSet botGot, final JSONObject bot,
			final JSONObject speech, final JSONObject avatar)
	throws JSONException, SQLException {
		bot.put ("avatar", avatar);
		bot.put ("speech", speech);
		bot.put ("startX", botGot.getInt ("x"));
		bot.put ("startY", botGot.getInt ("y"));
		bot.put ("facing", botGot.getString ("facing"));
		bot.put ("walkFreq", rs.getInt ("walkFreq"));
		bot.put ("talkFreq", rs.getInt ("talkFreq"));
	}

	/**
	 * TODO: document this method (brpocock, Dec 29, 2009)
	 * 
	 * @param rs WRITEME
	 * @param clothes WRITEME
	 * @throws JSONException WRITEME
	 * @throws SQLException WRITEME
	 */
	private void superbot_fetchClothing (final ResultSet rs,
			final JSONObject clothes)
	throws JSONException, SQLException {
		int i = 1;
		for (final String layer : ClothingItem.getClothingLayerNames ()) {
			if (layer.equals ("back") || layer.equals ("pants")
					|| layer.equals ("shirt") || layer.equals ("neck")
					|| layer.equals ("eyes") || layer.equals ("ears")
					|| layer.equals ("top") || layer.equals ("pivitz")) {
				final JSONObject item = new JSONObject ();
				item.put ("id", rs.getInt (layer));
				if ( !rs.wasNull ()) {
					clothes.put (String.valueOf (i++ ), item);
				}
			}
		}
	}

	/**
	 * TODO: document this method (brpocock, Dec 29, 2009)
	 * 
	 * @param rs WRITEME
	 * @param clothes WRITEME
	 * @throws JSONException WRITEME
	 * @throws SQLException WRITEME
	 */
	private void superbot_fetchPattern (final ResultSet rs,
			final JSONObject clothes)
	throws JSONException, SQLException {
		final JSONObject pat = new JSONObject ();
		pat.put ("id", rs.getInt ("pattern"));
		pat.put ("color", rs.getInt ("patternColor"));
		clothes.put ("0", pat);
	}

	/**
	 * TODO: document this method (brpocock, Dec 29, 2009)
	 * 
	 * @param getSpeech WRITEME
	 * @param rs WRITEME
	 * @return WRITEME
	 * @throws SQLException WRITEME
	 * @throws JSONException WRITEME
	 */
	private JSONObject superbot_fetchSpeech (
			final PreparedStatement getSpeech, final ResultSet rs)
	throws SQLException, JSONException {
		final JSONObject speech = new JSONObject ();

		getSpeech.setString (1, rs.getString ("botName"));
		if (getSpeech.execute ()) {
			final ResultSet words = getSpeech.getResultSet ();

			int s = 0;
			while (words.next ()) {
				speech.put (String.valueOf (s++ ), words
						.getString ("text"));
			}
		}
		return speech;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.AbstractRoom#toJSON()
	 */
	@Override
	public JSONObject toJSON () {
		final JSONObject self = new JSONObject ();
		try {
			self.put ("filename", filename);
			self.put ("moniker", moniker);
			self.put ("overlay", overlay);
			self.put ("sky", sky);
			self.put ("skyVisible", skyVisible);
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 18, 2009)
			AppiusClaudiusCaecus.reportBug (e);
		}
		return self;
	}

	/**
	 * TODO: document this method (brpocock, Oct 23, 2009)
	 */
	private void validateUserList () {
		for (final Integer uid : userList) {
			final AbstractUser u = User.getByID (uid);
			if (null == u) {
				userList.remove (uid);
			} else if ( !u.isOnline ()) {
				part (u);
			} else if (null == u.getRoom ()) {
				u.setRoom (this);
			} else if ( !u.getRoom ().equals (this)) {
				part (u);
			}
		}
	}

}
