/**
 * Copyright © 2009-2010, Bruce-Robert Pocock & Res Interactive, LLC.
 * All Rights Reserved. Licensed for perpetual use, modification, and/or
 * distribution by either party.
 * 
 * @author brpocock
 */
package com.tootsville.user;

import java.io.IOException;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLDataException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Vector;
import java.util.Map.Entry;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.AlreadyExistsException;
import org.starhope.appius.except.AlreadyUsedException;
import org.starhope.appius.except.DataException;
import org.starhope.appius.except.ForbiddenUserException;
import org.starhope.appius.except.GameLogicException;
import org.starhope.appius.except.NonSufficientFundsException;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.except.PrivilegeRequiredException;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.game.AbstractRoom;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.game.inventory.ClothingItem;
import org.starhope.appius.game.inventory.InventoryItem;
import org.starhope.appius.game.inventory.TootBookTheme;
import org.starhope.appius.mb.UserAddress;
import org.starhope.appius.mb.UserEnrolment;
import org.starhope.appius.types.AbstractZone;
import org.starhope.appius.types.AgeBracket;
import org.starhope.appius.types.GameWorldMessage;
import org.starhope.appius.types.ItemCreationTemplate;
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 sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import com.tootsville.promo.PeanutCode;
import com.tootsville.promo.Promotion;

/**
 * A Toot™ character as a specific user within Tootsville™. Contains
 * Tootsville-specific methods that don't belong in the GPL core of
 * Appius. Some of these may be changed to deprecated replacements for
 * the GPL functionality when new ideas, such as currency possessions,
 * are incorporated into the base class.
 * 
 * @author brpocock
 */
public class Toot extends User {

	/**
	 * Java® serialization unique ID
	 */
	private static final long serialVersionUID = 5113003589233884081L;

	/**
	 * Find the user who is uniquely identified by the given approval
	 * cookie
	 * 
	 * @param cookie The approval cookie used in the eMail
	 * @return the user identified by the cookie
	 * @throws IOException if the cookie can't be decoded
	 * @throws NotFoundException if the user can't be identified
	 */
	public static Toot getByApprovalCookie (final String cookie)
			throws IOException, NotFoundException {
		final BASE64Decoder decoder = new BASE64Decoder ();
		final String info = new String (decoder.decodeBuffer (cookie));
		final String [] infos = info.split ("/");
		if (infos.length != 3)
			throw new NotFoundException (cookie);
		AppiusClaudiusCaecus.blather ("DECODE approval cookie ID: "
				+ infos [0] + " Basic 8: " + infos [1] + " User Name: "
				+ infos [2]);
		final Toot tryThis = (Toot) User.getByID (Integer
				.parseInt (infos [0]));
		if (null == tryThis
				|| tryThis.getBasic8Choice () != Integer
						.parseInt (infos [1])
				|| !tryThis.getUserName ().toLowerCase (Locale.ENGLISH)
						.equals (infos [2]))
			throw new NotFoundException (cookie);
		return tryThis;
	}

	/**
	 * @param id the message ID to which replies are wanted
	 * @return mail messages posted on the user's wall in reply to the
	 *         given message
	 */
	public static Vector <GameWorldMessage> getMailOnWallInReplyTo (
			final int id) {
		Vector <GameWorldMessage> messages = new Vector <GameWorldMessage> ();
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT * FROM messages WHERE isDeleted='W' AND inReplyTo=? AND body<>'' ORDER BY messages.sentTime ASC");

			st.setInt (1, id);
			messages = AppiusConfig.newGameWorldMessage ()
					.getMessagesFrom (st);
		} 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 */
				}
			}
		}
		return messages;
	}

	/**
	 * The Toot from the Basic 8 that they originally chose.
	 */
	private int basic8Choice = 1;

	/**
	 * WRITEME: document this field (brpocock, Jan 8, 2010) gun (Toot)
	 */
	private TooterShooter gun;
	/**
	 * House type ID
	 */
	private int houseTypeID = -1;
	/**
	 * Lot (neighborhood) ID
	 */
	private int lotID = -1;
	/**
	 * The number of peanuts (currency) that this user has.
	 */
	private BigDecimal peanuts = BigDecimal.ZERO;
	/**
	 * The number of hours remaining of this user's Toot Timer
	 */
	private int tootTimeLeft;
	/**
	 * The number of minutes (beyond the hour) remaining of this user's
	 * Toot Timer
	 */
	private int tootTimeLeftMinutes;
	/**
	 * If true, the user's Toot Timer will refill every day CV
	 * {@link #tootTimerMonth}
	 */
	private boolean tootTimerDay;
	/**
	 * The number of hours which are added to the Toot Timer on each
	 * refill.
	 */
	private int tootTimeRefill;

	/**
	 * If true, the user's Toot Timer will refill every month. CV
	 * {@link #tootTimerDay}
	 */
	private boolean tootTimerMonth;

	/**
	 * WRITEME
	 * 
	 * @param birthDate1 WRITEME
	 * @param avatarTitle WRITEME
	 * @param userNameRequest WRITEME
	 * @throws AlreadyUsedException WRITEME
	 * @throws ForbiddenUserException WRITEME
	 * @throws NumberFormatException WRITEME
	 * @see User#User(Date, String, String)
	 */
	public Toot (final Date birthDate1, final String avatarTitle,
			final String userNameRequest) throws AlreadyUsedException,
			ForbiddenUserException, NumberFormatException {
		super (birthDate1, avatarTitle, userNameRequest);
		basic8Choice = getAvatarClass ().getID ();
	}

	/**
	 * WRITEME
	 * 
	 * @param id WRITEME
	 * @throws NotFoundException WRITEME
	 * @see User#User(int)
	 */
	public Toot (final int id) throws NotFoundException {
		super (id);
	}

	/**
	 * WRITEME
	 * 
	 * @param rs WRITEME
	 */
	public Toot (final ResultSet rs) {
		super (rs);
		try {
			setSubclassValues (rs);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.fatalBug (
					"Unable to set subclass values", e);
		}
	}

	/**
	 * WRITEME
	 * 
	 * @param newUserLogin WRITEME
	 * @throws NotFoundException WRITEME
	 * @see User#User(String)
	 */
	public Toot (final String newUserLogin) throws NotFoundException {
		super (newUserLogin);
	}

	/**
	 * Take a peanut code and apply the benefits thereof to this user
	 * 
	 * @param peanutSerial the serial number / code sequence in the
	 *        database
	 * @return a user-visible message explaining the benefits that were
	 *         applied
	 * @throws NotFoundException if the peanut code serial number is not
	 *         found
	 * @throws AlreadyUsedException if that peanut code has already been
	 *         used
	 */
	public String acceptPeanutCode (final String peanutSerial)
			throws NotFoundException, AlreadyUsedException {
		final String peanutCode = peanutSerial
				.toLowerCase (Locale.ENGLISH);
		final BigDecimal nuts = PeanutCode.redeemPeanutCode (this,
				peanutCode);
		try {
			addPeanuts (nuts, "givenuts"/* FIXME: wrong moniker? */);
		} catch (final AlreadyExistsException e) {
			AppiusClaudiusCaecus.reportBug ("Unabled to give Toot ID#"
					+ getUserID () + " " + nuts.toPlainString ()
					+ " peanuts for peanut code " + peanutCode);
		}
		final String prefix = peanutSerial.substring (0, 3);
		try {
			final Promotion promo = Promotion.getByLink (prefix);
			setReferer (promo.getCode ());
		} catch (final NotFoundException e) {
			// ignore, not a promo peanut code.
		}
		return "You got " + nuts.intValue () + " peanuts";
	}

	/**
	 * WRITEME: document this method (theys, Sep 10, 2009)
	 * 
	 * @param add additional peanuts given
	 * @param moniker reason for peanuts
	 * @throws AlreadyExistsException WRITEME
	 */
	public void addPeanuts (final BigDecimal add, final String moniker)
			throws AlreadyExistsException {
		final int eventID = startEventRaw (moniker);
		try {
			endEvent (eventID, moniker, add, null);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#affirmFreeMember()
	 */
	@Override
	protected void affirmFreeMember () {
		super.affirmFreeMember ();
		blog ("free member; removing clothes and patterns and reverting to basic 8 avatar");
		setAvatarClass (new AvatarClass (basic8Choice));
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#affirmPaidMember()
	 */
	@Override
	public void affirmPaidMember () {
		super.affirmPaidMember ();
		if (getAvatarClass ().getID () >= 1
				&& getAvatarClass ().getID () <= 8) {
			final AvatarClass oldAvatar = AvatarClass
					.getByID (basic8Choice);
			setAvatarClass (AvatarClass.getByID (9));
			setBaseColor (oldAvatar.getDefaultBaseColor ());
			setExtraColor (oldAvatar.getDefaultExtraColor ());
			final ClothingItem pattern = oldAvatar.getDefaultPattern ();
			blog ("basic 8:" + basic8Choice + " avatar: "
					+ oldAvatar.toString () + " pattern: "
					+ oldAvatar.getDefaultPattern ());
			addDefaultFreeItem (pattern.getID ());
			wear (pattern);
		}
	}

	/**
	 * Send a biff message to announce the number of message in a user's
	 * Inbox.
	 * 
	 * @param abstractZone The zone in which the user is standing
	 * 
	 * @param room The room in which the user is standing
	 * @throws JSONException If the biff can't be sent
	 */
	public void biff (final AbstractZone abstractZone,
			final AbstractRoom room) throws JSONException {
		final JSONObject results = new JSONObject ();
		results.put ("newMail", getInboxCount ());
		acceptSuccessReply ("postman", results, room);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#doTransport()
	 */
	@Override
	public void doTransport () {
		getZone ().handleSpeak (getRoomNumber (), this, "/zapattack");
	}

	/**
	 * Close out an event started with {@link #startEvent(String)}.
	 * Handles "fountain" events specially by giving a random score from
	 * 1..100.
	 * 
	 * @param eventID The ID of the event
	 * @param moniker the event moniker to be ended. Not used
	 *        internally, but returned in the JSON result set.
	 * @param rawScore The earned score (points, not peanuts). Ignored
	 *        for fountains.
	 * @param medalMoniker The moniker of the medal earned, if any
	 * @return JSON data describing the completed event and high score
	 *         data for that type of event.
	 * @throws JSONException if the JSON data can't be represented
	 *         correctly.
	 */
	@Override
	public JSONObject endEvent (final int eventID,
			final String moniker, final BigDecimal rawScore,
			final String medalMoniker) throws JSONException {

		final JSONObject result = new JSONObject ();
		result.put ("eventID", eventID);

		BigDecimal score;
		BigDecimal peanutsNum;
		PreparedStatement getPeanutScore = null;

		if (moniker.startsWith ("fountain")) {
			score = new BigDecimal (AppiusConfig.getRandomInt (10, 40));
			blog ("Random score for fountain: "
					+ score.toPlainString ());
			peanutsNum = score;
		} else {
			score = rawScore;
			Connection con = null;
			try {
				con = AppiusConfig.getDatabaseConnection ();
				getPeanutScore = con
						.prepareStatement ("SELECT ( ? * pointToPeanut ) AS peanuts"
								+ "  FROM eventTypes"
								+ " WHERE ID = ( SELECT eventTypeID"
								+ "                FROM events"
								+ "               WHERE events.ID = ? )");

				getPeanutScore.setLong (1, score.longValue ());
				getPeanutScore.setInt (2, eventID);
				getPeanutScore.execute ();
				final ResultSet getPeanuts = getPeanutScore
						.getResultSet ();
				getPeanuts.next ();
				peanutsNum = new BigDecimal (getPeanuts
						.getLong ("peanuts")).setScale (0);
			} catch (final SQLException e) {
				AppiusClaudiusCaecus.reportBug (e);
				peanutsNum = new BigDecimal (10);

			} finally {
				if (null != getPeanutScore) {
					try {
						getPeanutScore.close ();
					} catch (final SQLException e) {
						/* No Op */
					}
				}
				if (null != con) {
					try {
						con.close ();
					} catch (final SQLException e) { /* No Op */
					}
				}

			}
		}

		if (peanuts.add (peanutsNum).compareTo (BigDecimal.ZERO) < 0) {
			peanutsNum = peanuts.negate ();
		}

		setPeanuts (peanuts.add (peanutsNum));

		result.put ("totalPeanuts", getPeanuts ().toPlainString ());
		result.put ("peanuts", peanutsNum);
		PreparedStatement endEvent = null;
		Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			endEvent = con.prepareStatement ("UPDATE events "
					+ " SET completionTimestamp = NOW(), "
					+ "     points = ?, peanuts = ?,"
					+ "     medalType = (SELECT ID"
					+ "              FROM medalTypes"
					+ "             WHERE name=?)" + " WHERE ID = ?");
			endEvent.setBigDecimal (1, score);
			endEvent.setBigDecimal (2, peanutsNum);
			endEvent.setString (3, medalMoniker);
			endEvent.setInt (4, eventID);
			endEvent.execute ();

			putEventHighScoresIntoJSON (eventID, result);

		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			return new JSONObject ();
		} finally {
			if (null != endEvent) {
				try {
					endEvent.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return result;
	}

	/**
	 * This is the raw, internal implementation code used to terminate
	 * an event. This is used for purchases at least … WRITEME… Is this
	 * used for minigames? multiplayer games? fountains?
	 * 
	 * @param eventID The event unique ID assigned when the event was
	 *        started
	 * @param item any StoreItem gained as a result of the transaction
	 * @throws NotFoundException WRITEME
	 * @throws NonSufficientFundsException if the user can't afford the
	 *         store item
	 * @throws AlreadyExistsException if the user is trying to
	 *         buy/obtain the same item twice
	 */
	@Override
	public void endEventPurchaseRaw (final int eventID,
			final ItemCreationTemplate item) throws NotFoundException,
			NonSufficientFundsException, AlreadyExistsException {
		if (item.getPrice ().compareTo (peanuts) > 0)
			throw new NonSufficientFundsException (peanuts
					.subtract (item.getPrice ()));

		final InventoryItem invItem = InventoryItem.getByID (item
				.getItemID ());
		if (!invItem.isFurniture ()) {
			for (final InventoryItem thing : getInventory ())
				if (thing.getID () == invItem.getID ())
					throw new AlreadyExistsException (invItem.toJSON ()
							.toString ());
		}

		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("UPDATE events SET completionTimestamp=NOW(), points=?, peanuts=?, itemGained=? WHERE ID=? AND creatorID=?");
			final BigDecimal price = BigDecimal.ZERO.subtract (item
					.getPrice ());

			st.setInt (1, item.getPrice ().intValue ());
			st.setInt (2, price.intValue ());
			st.setInt (3, item.getItemID ());
			st.setInt (4, eventID);
			st.setInt (5, userID);
			if (st.executeUpdate () == 0)
				throw new NotFoundException (String.valueOf (eventID));
			setPeanuts (peanuts.add (price));
			addItem (invItem);
		} catch (final SQLException e) {
			// Default catch action, report bug (brpocock, Aug 31, 2009)
			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 */
				}
			}
		}
	}

	/**
	 * TODO: document this method (brpocock, Nov 13, 2009)
	 * 
	 * @param eventID WRITEME
	 * @param moniker WRITEME
	 * @param gameCode WRITEME
	 * @param score WRITEME
	 * @param scores WRITEME
	 * @return WRITEME
	 * @throws JSONException WRITEME
	 */
	@Override
	public JSONObject endMultiplayerEvent (final int eventID,
			final String moniker, final String gameCode,
			final BigDecimal score,
			final LinkedHashMap <Integer, Integer> scores)
			throws JSONException {

		final JSONObject result = new JSONObject ();
		result.put ("eventID", eventID);

		setPeanuts (peanuts.add (score));
		result.put ("totalPeanuts", getPeanuts ().toPlainString ());
		result.put ("peanuts", score);
		result.put ("moniker", moniker);
		result.put ("gameCode", gameCode);
		result.put ("players", scores.size ());
		Connection con = null;
		PreparedStatement endEvent = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			endEvent = con.prepareStatement ("UPDATE events "
					+ " SET completionTimestamp = NOW(), "
					+ "     points = ?, peanuts = ?,"
					+ "     medalType = '' WHERE ID = ?");
			endEvent.setBigDecimal (1, score);
			endEvent.setBigDecimal (2, score);
			endEvent.setInt (3, eventID);
			endEvent.execute ();

			final JSONObject highScoreList = new JSONObject ();

			int place = scores.size ();
			for (final Entry <Integer, Integer> thatScore : scores
					.entrySet ()) {
				final JSONObject scoreInfo = new JSONObject ();
				scoreInfo.put ("points", thatScore.getValue ());
				scoreInfo.put ("userName", User
						.getUserNameForID (thatScore.getKey ()));
				highScoreList
						.put (String.valueOf (place-- ), scoreInfo);
			}
			result.put ("highScores", highScoreList);

		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			return new JSONObject ();
		} finally {
			if (null != endEvent) {
				try {
					endEvent.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
		return result;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#fetch_avatarInfo(java.sql.ResultSet)
	 */
	@Override
	protected void fetch_avatarInfo (final ResultSet resultSet)
			throws SQLException {
		super.fetch_avatarInfo (resultSet);
		try {
			basic8Choice = Integer.parseInt (resultSet
					.getString ("basic8Choice"));
		} catch (final NumberFormatException e) {
			basic8Choice = 6;
		}
	}

	/**
	 * Load the user's gun from the result set
	 * 
	 * @see Toot#fetch_more(ResultSet)
	 * @param resultSet The record describing this Toot
	 */
	private void fetch_gun (final ResultSet resultSet) {
		try {
			final int gunID = resultSet.getInt ("gunID");
			if (resultSet.wasNull ()) {
				gun = null;
			} else {
				gun = TooterShooterEnum.getTooterShooter (gunID);
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a SQLException in fetch_gun", e);
		}
	}

	/**
	 * Fetch the user's house information from the database record
	 * 
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_house () throws SQLException {
		lotID = houseTypeID = -1;
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT * FROM userHouses WHERE userID=? LIMIT 1");
			st.setInt (1, userID);
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ()) {
					lotID = rs.getInt ("lotID");
					houseTypeID = rs.getInt ("houseID");
				}
			}
		} 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.user.User#fetch_more(java.sql.ResultSet)
	 */
	@Override
	protected void fetch_more (final ResultSet resultSet)
			throws SQLException {
		fetch_house ();
		fetch_gun (resultSet);
	}

	/**
	 * Write any changes to database
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#flush()
	 */
	@Override
	public void flush () {
		blog ("Flushing changes");

		if (AppiusConfig
				.getConfigBoolOrFalse ("com.resinteractive.theys.traceUserFlush")) {
			AppiusClaudiusCaecus.traceThis ();
		}

		// Don't try to save without at least a requested name.
		if (null == getLogin () && null == getRequestedName ()) {
			blog (" Can't save anonymous user record");
			return;
		}

		Connection con = null;
		PreparedStatement update = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			// FIXME: Organize fields into a more manageable order in
			// the UPDATE statement...!
			update = con
					.prepareStatement ("UPDATE users SET userName=?, password=?, avatarClass=?, baseColor=?, extraColor=?, "
							+ "mail=?, mailConfirmed=?, birthDate=?, ageGroup=?, language=?, dialect=?, parentID=?, "
							+ "approvedDate=?, emailPlusDate=?, canTalk=?, canEnterChatZone=?, canEnterMenuZone=?, "
							+ "givenName=?, kickedUntil=?, kickedReasonCode=?, kickedBy=?, isActive=?, needsNaming=?, "
							+ "staffLevel=?, canContact=?, requestedName=?, basic8Choice=?, "
							+ "passRecoverQ=?, passRecoverA=?, peanuts=?, nameRequestedAt=?, requestedName=?, "
							+ "referer=?, chatBG=?, chatFG=? "
							+ "WHERE ID=?");

			flush_breakOut_loginPass (update);
			flush_breakOut_avatar (update);
			flush_breakOut_mail (update);
			flush_breakOut_dob (update);
			flush_breakOut_language (update);
			flush_breakOut_parent (update);
			flush_breakOut_permissions (update);
			flush_breakOut_nameRequest (update);
			flush_breakOut_referer (update, 33);
			flush_breakOut_chatColours (update, 34, 35);

			update.setBigDecimal (30, peanuts);

			update.setInt (36, userID);

			blog (update.executeUpdate ()
					+ " <--- actual changes to core record\n");
			final SQLWarning w = update.getWarnings ();
			if (null != w) {
				w.printStackTrace ();
			}

		} catch (final SQLException e) {
			AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != update) {
				try {
					update.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		/* ------ */
		flush_userHouse ();
	}

	/**
	 * TODO: document this method (brpocock, Sep 23, 2009)
	 * 
	 * @param update WRITEME
	 * @throws SQLException WRITEME
	 */
	private void flush_breakOut_avatar (final PreparedStatement update)
			throws SQLException {
		update.setInt (3, getAvatarClass ().getID ());
		update.setLong (4, getBaseColor ().toLong ());
		update.setLong (5, getExtraColor ().toLong ());
		update.setString (27, String.valueOf (basic8Choice));
	}

	/**
	 * TODO: document this method (brpocock, Sep 23, 2009)
	 */
	private void flush_userHouse () {
		if (lotID > -1) {
			Connection con = null;
			PreparedStatement delOld = null;
			PreparedStatement updateLot = null;
			try {
				con = AppiusConfig.getDatabaseConnection ();
				delOld = con
						.prepareStatement ("DELETE FROM userHouses WHERE userID=?");
				delOld.setInt (1, userID);
				delOld.execute ();
				updateLot = con
						.prepareStatement ("INSERT INTO userHouses (userID, lotID, houseID) VALUES (?,?,?)");
				updateLot.setInt (1, userID);
				updateLot.setInt (2, lotID);
				updateLot.setInt (3, houseTypeID);
				assert updateLot.execute ();
			} catch (final SQLException e) {
				AppiusClaudiusCaecus.reportBug (e);
			} finally {
				if (null != delOld) {
					try {
						delOld.close ();
					} catch (final SQLException e) { /* No Op */
					}
				}
				if (null != updateLot) {
					try {
						updateLot.close ();
					} catch (final SQLException e) { /* No Op */
					}
				}
				if (null != con) {
					try {
						con.close ();
					} catch (final SQLException e) { /* No Op */
					}
				}
			}
		}
	}

	/**
	 * Retrieve a number of of MailMessages starting with the most
	 * recent.
	 * 
	 * @param numberOfMessages The number of posts needed.
	 * @return mail messages posted on the user's wall
	 */
	public Vector <GameWorldMessage> getAllMailOnWall (
			final int numberOfMessages) {
		return getAllMailOnWall (0, numberOfMessages);
	}

	/**
	 * Retrieve a number of of MailMessages starting with the most
	 * recent.
	 * 
	 * @param numberOfMessages The number of posts needed.
	 * @param start WRITEME
	 * @return mail messages posted on the user's wall
	 */
	public Vector <GameWorldMessage> getAllMailOnWall (
			final int numberOfMessages, final int start) {
		return getMailOnWallAndBuddyWall (start, numberOfMessages);
	}

	/**
	 * @return Get a cookie to be used in creating the user's
	 *         parent/self-approval eMails.
	 */
	@Override
	public String getApprovalCookie () {
		final BASE64Encoder encoder = new BASE64Encoder ();
		return encoder
				.encodeBuffer ( (String.valueOf (userID) + "/"
						+ String.valueOf (basic8Choice) + "/" + getRequestedName ()
						.toLowerCase (Locale.ENGLISH)).getBytes ());
	}

	/**
	 * @return the basic8Choice
	 */
	public int getBasic8Choice () {
		// default getter (brpocock, Aug 28, 2009)
		return basic8Choice;
	}

	/**
	 * TODO: document this method (brpocock, Jan 8, 2010)
	 * 
	 * @return WRITEME
	 */
	protected TooterShooter getGun () {
		return gun;
	}

	/**
	 * Get the type of house (exterior frame)
	 * 
	 * @return the ID of the house exterior frame chosen by the user, or
	 *         -1 if the user hasn't chosen one yet.
	 */
	public int getHouseTypeID () {
		return houseTypeID;
	}

	/**
	 * @return WRITEME
	 */
	public int getInboxCount () {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet inboxCount = null;
		int count = 0;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT COUNT(*) FROM messages WHERE readTime IS NULL AND isDeleted=' ' AND toUserID=?");

			st.setInt (1, getUserID ());
			inboxCount = st.executeQuery ();
			inboxCount.next ();
			count = inboxCount.getInt (1);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus
					.reportBug ("Can't get unread message count for "
							+ getLogin (), e);
		} finally {
			if (null != inboxCount) {
				try {
					inboxCount.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 count;
	}

	/**
	 * Get the ID of the type of lot (neighbourhood) for the user's
	 * house. (Usually not set)
	 * 
	 * @return The lot ID
	 */
	public int getLotID () {
		return lotID;
	}

	/**
	 * @return mail messages in the user's inbox
	 */
	public Vector <GameWorldMessage> getMailInBox () {
		Vector <GameWorldMessage> messages = new Vector <GameWorldMessage> ();
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT * FROM messages WHERE toUserID=? AND isDeleted=' ' ORDER BY messages.sentTime DESC");

			st.setInt (1, getUserID ());
			messages = AppiusConfig.newGameWorldMessage ()
					.getMessagesFrom (st);
		} 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 */
				}
			}

		}
		return messages;
	}

	/**
	 * Retrieve a number of of MailMessages starting with the most
	 * recent.
	 * 
	 * @param numberOfMessages The number of posts needed.
	 * @return mail messages posted on the user's wall
	 */
	public Vector <GameWorldMessage> getMailOnMyWall (
			final int numberOfMessages) {
		return getMailOnMyWall (0, numberOfMessages);
	}

	/**
	 * Retrieve a number of of MailMessages starting with the most
	 * recent.
	 * 
	 * @param numberOfMessages The number of posts needed.
	 * @param start WRITEME
	 * @return mail messages posted on the user's wall
	 */
	public Vector <GameWorldMessage> getMailOnMyWall (
			final int numberOfMessages, final int start) {
		return getMailOnWall (numberOfMessages, start);
	}

	/**
	 * @param offset the first message to fetch (counting from 0 being
	 *        the user's first wall message ever)
	 * @param limit the last message to fetch. If there are fewer
	 *        messages than this index, you may receive less results.
	 * @return mail messages posted on the user's wall
	 */
	private Vector <GameWorldMessage> getMailOnWall (final int limit,
			final int offset) {
		Vector <GameWorldMessage> messages = new Vector <GameWorldMessage> ();
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT * FROM messages WHERE toUserID=? AND isDeleted='W' AND inReplyTo IS NULL AND body<>'' ORDER BY messages.sentTime DESC LIMIT ? OFFSET ?");
			st.setInt (1, getUserID ());
			st.setInt (2, limit);
			st.setInt (3, offset);
			messages = AppiusConfig.newGameWorldMessage ()
					.getMessagesFrom (st);
		} 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 */
				}
			}
		}
		return messages;
	}

	/**
	 * <pre>
	 * theys Jan 15, 2010
	 * </pre>
	 * 
	 * TO getMailOnWallAndBuddyWall WRITEME...
	 * 
	 * @param offset WRITEME theys
	 * @param limit WRITEME theys
	 * @return WRITEME theys
	 */
	private Vector <GameWorldMessage> getMailOnWallAndBuddyWall (
			final int offset, final int limit) {
		Vector <GameWorldMessage> messages = new Vector <GameWorldMessage> ();
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT * FROM messages WHERE (toUserID IN (SELECT buddyID FROM buddyList WHERE userID=?) OR toUserID=?) AND fromUserID=toUserID AND isDeleted='W' AND inReplyTo IS NULL AND body<>'' ORDER BY messages.sentTime DESC LIMIT ? OFFSET ?");
			st.setInt (1, getUserID ());
			st.setInt (2, getUserID ());
			st.setInt (3, limit);
			st.setInt (4, offset);
			messages = AppiusConfig.newGameWorldMessage ()
					.getMessagesFrom (st);
		} 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 */
				}
			}
		}
		return messages;
	}

	/**
	 * @return the amount of peanuts (currency) that this user has
	 */
	public BigDecimal getPeanuts () {
		return peanuts;
	}

	/**
	 * @return The last post this user wrote on their own wall.
	 */
	public String getTootBookStatus () {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("SELECT body FROM messages WHERE toUserID=? AND isDeleted='W' AND fromUserID=toUserID AND inReplyTo IS NULL AND body<>'' ORDER BY sentTime DESC LIMIT 1");

			st.setInt (1, getUserID ());
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ())
					return rs.getString ("body");
			}
		} 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 "I haven't thought of anything to put here yet.";
	}

	/**
	 * @param slotNumber WRITEME
	 * @return WRITEME
	 * @throws NotFoundException WRITEME
	 */
	public TootBookTheme getTootBookThemeItemBySlot (
			final int slotNumber) throws NotFoundException {
		final Iterator <InventoryItem> inv = getInventory ()
				.iterator ();
		while (inv.hasNext ()) {
			final InventoryItem item = inv.next ();
			if (item.isTootBookTheme ()) {
				final TootBookTheme tootBookTheme = item
						.asTootBookThemeItem ();
				if (slotNumber == tootBookTheme.getSlotNumber ())
					return tootBookTheme;
			}
		}
		blog (" No furniture in slot " + slotNumber
				+ " is found in inventory");
		throw new NotFoundException (String.valueOf (slotNumber));
	}

	/**
	 * @return hash map of display names and URLs of available themes
	 *         that this user owns
	 */
	@Deprecated
	public HashMap <String, String> getTootsBookThemes () {
		return new HashMap <String, String> ();
	}

	/**
	 * Get the URL for the active TootBook theme folder for this user.
	 * 
	 * @return relative URL for the theme folder
	 */
	public String getTootsBookThemeURL () {
		return "default";
	}

	/**
	 * @return the tootTimeLeft
	 */
	public int getTootTimeLeft () {
		return tootTimeLeft;
	}

	/**
	 * @return the toot time remaining as hours & minutes delimited by
	 *         "hr " and "min"
	 */
	public String getTootTimeLeft$ () {
		return tootTimeLeft + "hr " + tootTimeLeftMinutes + "min";
	}

	/**
	 * @return the tootTimeLeftMinutes
	 */
	public int getTootTimeLeftMinutes () {
		return tootTimeLeftMinutes;
	}

	/**
	 * @return the tootTimeRefill
	 */
	public int getTootTimeRefill () {
		return tootTimeRefill;
	}

	/**
	 * @return WRITEME
	 */
	public String getURL () {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * Give a gift of peanuts. Or, take away peanuts. This sends the
	 * Earnings balloon message to the user.
	 * 
	 * @param numNuts WRITEME
	 * @param event event string
	 * @throws AlreadyExistsException WRITEME
	 * @throws JSONException WRITEME
	 */
	public void giftPeanuts (final BigDecimal numNuts,
			final String event) throws AlreadyExistsException,
			JSONException {
		addPeanuts (numNuts, event);
		if (isOnline ())
			if (numNuts.compareTo (BigDecimal.ZERO) > 0) {
				sendEarnings (getRoom (), "You got "
						+ numNuts.toString () + " peanuts!");
			} else {
				sendEarnings (getRoom (), "You lost "
						+ numNuts.negate ().toString () + " peanuts!");
			}
	}

	/**
	 * TODO: document this method (brpocock, Dec 30, 2009)
	 * 
	 * @param birthDate1 WRITEME
	 * @see User#insertIntoDatabase(Date)
	 */
	@Override
	protected void insertIntoDatabase (final Date birthDate1) {
		PreparedStatement insert = null;
		userID = -1;
		Connection con = null;
		ResultSet keys = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			insert = con
					.prepareStatement (
							"INSERT INTO users (birthDate, ageGroup, approvedDate, canEnterChatZone, canEnterMenuZone, canTalk, avatarClass, baseColor, extraColor, requestedName, registeredAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())",
							Statement.RETURN_GENERATED_KEYS);

			insert.setDate (1, birthDate1);
			final AgeBracket tootAgeGroup = getAgeGroup ();
			insert.setString (2, tootAgeGroup == AgeBracket.Kid ? "K"
					: tootAgeGroup == AgeBracket.Adult ? "A"
							: tootAgeGroup == AgeBracket.Teen ? "T"
									: "X");
			if (getApprovedDate () == null) {
				insert.setNull (3, java.sql.Types.DATE);
			} else {
				insert.setDate (3, getApprovedDate ());
			}
			insert.setString (4, isCanEnterChatZone () ? "Y" : "N");
			insert.setString (5, isCanEnterMenuZone () ? "Y" : "N");
			insert.setString (6, isCanTalk () ? "Y" : "N");
			insert.setInt (7, getAvatarClass ().getID ());
			insert.setLong (8, getBaseColor ().toLong ());
			insert.setLong (9, getExtraColor ().toLong ());
			insert.setString (10, getRequestedName ());

			// insert.execute ();
			if (insert.executeUpdate () != 1)
				throw new SQLException ("adding new user failed with "
						+ insert.getUpdateCount () + " updates");
			keys = insert.getGeneratedKeys ();
			if (keys.next ()) {
				userID = keys.getInt (1);
			} else
				throw new SQLException ("Can't get user ID");
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != keys) {
				try {
					keys.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != insert) {
				try {
					insert.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
	}

	/**
	 * @return true, if the Toot Timer resets every day for this user.
	 */
	public boolean isTootTimerDay () {
		// default getter (brpocock, Jul 8, 2009)
		return tootTimerDay;
	}

	/**
	 * @return true, if the Toot Timer resets every week for this user.
	 */
	public boolean isTootTimerMonth () {
		// default getter (brpocock, Jul 8, 2009)
		return tootTimerMonth;
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#kick(org.starhope.appius.user.AbstractUser,
	 *      java.lang.String, int)
	 */
	public void kick (final AbstractUser u, final String kickReason,
			final int duration) throws PrivilegeRequiredException {
		// TODO Auto-generated method stub

	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#local_create()
	 */
	@Override
	public void local_create () {
		try {
			addPeanuts (new BigDecimal (AppiusConfig.getIntOrDefault (
					"com.tootsville.registration.freePeanuts", 500)),
					"registration");
		} catch (final AlreadyExistsException e3) {
			AppiusClaudiusCaecus.reportBug (e3);
		}

	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#local_publicInfo(org.json.JSONObject)
	 */
	@Override
	protected void local_publicInfo (final JSONObject userInfo) {
		try {
			final TooterShooter myGun = getGun ();
			if (null != myGun) {
				userInfo.put ("gunID", myGun.getID ());
			}
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in local_publicInfo", e);
		}
	}

	/**
	 * 
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.User#postLoginGlobal()
	 */
	@Override
	public void postLoginGlobal () {
		super.postLoginGlobal ();
		TooterShooterEnum.sendGunRunner (this);
	}

	/**
	 * Post a new message on this users TootBook wall.
	 * 
	 * @param toID The ID of the User who's being posted to
	 * @param body The body of the message being sent.
	 * @return The ID of the new post.
	 * @throws DataException If the replyToID does not resolve a
	 *         message, or if the message fails to send.
	 * @throws GameLogicException if the mail fails the filters. XXX
	 *         Filter exceptions
	 */
	public int postNewOnTootBookWall (final int toID, final String body)
			throws DataException, GameLogicException {
		return sendWallMail (toID, body, -1);
	}

	/**
	 * Post a reply to a post on a TootBook wall.
	 * 
	 * @param body The message to post.
	 * @param replyToID the ID of the post being replied to.
	 * @return The ID of the new post.
	 * @throws DataException If the replyToID does not resolve a
	 *         message, or if the message fails to send.
	 * @throws GameLogicException If the mail fails the filters. XXX
	 *         filter exceptions
	 */
	public int postReplyOnTootBookWall (final String body,
			final int replyToID) throws DataException,
			GameLogicException {
		try {
			return sendWallMail (AppiusConfig.newGameWorldMessage ()
					.getByID (replyToID).getToID (), body, replyToID);
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus
					.reportBug ("Unable to post a reply to Mail ID#"
							+ replyToID, e);
			throw new DataException ("No post exists with ID: "
					+ replyToID);
		}
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 28, 2009)
	 * 
	 * @param item WRITEME
	 * @throws AlreadyExistsException WRITEME
	 * @throws NotFoundException WRITEME
	 * @throws NonSufficientFundsException WRITEME
	 */
	public void purchase (final ItemCreationTemplate item)
			throws AlreadyExistsException, NotFoundException,
			NonSufficientFundsException {
		this.endEventPurchaseRaw (startEventRaw ("purchase"), item);
	}

	/**
	 * TODO: document this method (brpocock, Dec 1, 2009)
	 * 
	 * @param getHighScores WRITEME
	 * @param wrapper WRITEME
	 * @param eventID WRITEME
	 * @throws SQLException WRITEME
	 * @throws JSONException WRITEME
	 */
	private void pushHighScoresIntoJSON (
			final PreparedStatement getHighScores,
			final JSONObject wrapper, final int eventID)
			throws SQLException, JSONException {
		final JSONObject highScoreList = new JSONObject ();

		final ResultSet scoreFetcher = getHighScores.getResultSet ();
		int i = 1;
		while (scoreFetcher.next ()) {
			final JSONObject scoreInfo = new JSONObject ();
			final int thatID = scoreFetcher.getInt ("ID");
			BigDecimal scorePoints = scoreFetcher
					.getBigDecimal ("points");
			if (scoreFetcher.wasNull ()) {
				scorePoints = BigDecimal.ZERO;
			}
			if (scorePoints.compareTo (BigDecimal.ZERO) > 0) {
				if (eventID == thatID) {
					wrapper.put ("gotHighScore", i);
				}
				scoreInfo.put ("points", scorePoints.doubleValue ());
				scoreInfo.put ("ID", thatID);
				scoreInfo.put ("userName", scoreFetcher
						.getString ("userName"));
				highScoreList.put (String.valueOf (i++ ), scoreInfo);
			}
		}

		wrapper.put ("highScores", highScoreList);
	}

	/**
	 * Get high score information from the database and return it as a
	 * JSON object
	 * 
	 * @param eventID an event of the type for which we want the top 24
	 *        scores
	 * @param wrapper the JSON container to absorb the high score list
	 */
	protected void putEventHighScoresIntoJSON (final int eventID,
			final JSONObject wrapper) {
		PreparedStatement getHighScores = null;
		Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			getHighScores = con
					.prepareStatement ("SELECT events.ID AS ID, points, userName FROM events LEFT JOIN users ON users.ID = events.creatorID WHERE eventTypeID = ( SELECT eventTypeID FROM events AS e2 WHERE e2.ID = ? ) ORDER BY points DESC LIMIT 24");
			getHighScores.setInt (1, eventID);
			if (getHighScores.execute ()) {
				pushHighScoresIntoJSON (getHighScores, wrapper, eventID);
			}
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a Exception in getHighScores", e);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a Exception in getHighScores", e);
		} finally {
			if (null != getHighScores) {
				try {
					getHighScores.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * Send an earnings notice to the client. This pops up the blue
	 * “thought bubble” just like a fountain event.
	 * 
	 * @param room WRITEME
	 * @param msg WRITEME
	 */
	@Override
	public void sendEarnings (final AbstractRoom room, final String msg) {
		final JSONObject result = new JSONObject ();
		try {
			result.put ("msg", msg);
			result.put ("totalPeanuts", getPeanuts ().toString ());
			result.put ("status", true);
			result.put ("from", "earning");
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		final AppiusClaudiusCaecus serverThread = getServerThread ();
		if (null != serverThread) {
			try {
				serverThread.sendResponse (result, room.getID (), this);
			} catch (final UserDeadException e) {
				// don't care
			}
		}
	}

	/**
	 * Send an in-game eMail message
	 * 
	 * @param to The user to receive the message
	 * @param subject The subject of the message
	 * @param body The body/contents of the message
	 * @return true, if the message was sent successfully.
	 */
	private boolean sendMail (final int to, final String subject,
			final String body) {
		final GameWorldMessage msg = AppiusConfig
				.newGameWorldMessage ();
		msg.setFrom (this);
		msg.setSubject (subject);
		msg.setSent (new Timestamp (System.currentTimeMillis ()));
		msg.setBody (body);
		msg.setToID (to);
		if (isOnline () && null != getZone ()) {
			// TODO: tell Eaves
		}
		return msg.send ();
	}

	/**
	 * @param to WRITEME
	 * @param subject WRITEME
	 * @param body WRITEME
	 * @return WRITEME
	 */
	public boolean sendMail (final String to, final String subject,
			final String body) {
		final int toID = User.getIDForLogin (to);
		return this.sendMail (toID, subject, body);
	}

	/**
	 * Send a promotional plush Toot to this user, at the specified
	 * address.
	 * 
	 * @param shippingAddress the address to which to ship the plush
	 *        Toot
	 * @param tootSelect the Toot selected
	 * @throws SQLException if the order can't be inserted into shipping
	 * @throws GameLogicException if the user attempts to send the plush
	 *         Toot to someone else
	 */
	public void sendPlushTootPromo (final UserAddress shippingAddress,
			final int tootSelect) throws SQLException,
			GameLogicException {
		if (!shippingAddress.getUser ().equals (this))
			throw new GameLogicException ("refuse to send promo Toot ",
					shippingAddress.getUser (), this);
		PreparedStatement ship = null;

		Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			ship = con
					.prepareStatement ("INSERT INTO shipping (name, address, city, state, zip_code, toot, userID, ordered) VALUES (?,?,?,?,?,?,?,NOW())");
			ship.setString (1, getDisplayName ());
			ship.setString (2, shippingAddress.getAddressPair ());
			ship.setString (3, shippingAddress.getCity ());
			ship.setString (4, shippingAddress.getProvince ());
			ship.setString (5, shippingAddress.getPostalCode ());
			ship.setString (6, AvatarClass.getByID (tootSelect)
					.getString ());
			ship.setInt (7, getUserID ());
			ship.execute ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			throw e;
		} finally {
			if (null != ship) {
				try {
					ship.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}

		// TODO: Send a notification to the user stating that they are
		// receiving a promotional free Toot.
	}

	/**
	 * Creates a new post on a TootBook Profile.
	 * 
	 * @param toID The ID of the User who is receiving this post.
	 * @param body The body/contents of the message
	 * @param inReplyTo The ID of the post that is being replied to. -1
	 *        if this is not a reply.
	 * @return the ID of the new post.
	 * @throws DataException If the post fails to send.
	 * @throws GameLogicException If the post fails the filters.
	 */
	private int sendWallMail (final int toID, final String body,
			final int inReplyTo) throws DataException,
			GameLogicException {
		final GameWorldMessage msg = AppiusConfig
				.newGameWorldMessage ();
		msg.setFrom (this);
		msg.setSent (new Timestamp (System.currentTimeMillis ()));
		msg.setBody (body);
		msg.setWallPost (true);
		msg.setToID (toID);
		msg.setInReplyTo (inReplyTo);
		if (!msg.send ())
			throw new DataException ("Failed to post new wall message.");
		return msg.getID ();
	}

	/**
	 * @param basic8Choice1 the basic8Choice to set
	 */
	public void setBasic8Choice (final int basic8Choice1) {
		// default setter (brpocock, Aug 28, 2009)
		basic8Choice = basic8Choice1;
		changed ();
	}

	/**
	 * @param newGun the gun to set
	 */
	public void setGun (final TooterShooter newGun) {
		gun = newGun;
	}

	/**
	 * TODO: document this method (brpocock, Sep 24, 2009)
	 * 
	 * @param houseTypeID1 WRITEME
	 */
	public void setHouseTypeID (final int houseTypeID1) {
		houseTypeID = houseTypeID1;
		changed ();
	}

	/**
	 * @param newLotID WRITEME
	 */
	public void setLot (final int newLotID) {
		setLotID (newLotID);
	}

	/**
	 * Choose the lot ID (neighbourhood) in which this user wishes to
	 * build their new house. These are mapped to the room numbers of
	 * the primary areas
	 * 
	 * @param lotID1 WRITEME
	 */
	public void setLotID (final int lotID1) {
		lotID = lotID1;
		changed ();
	}

	/**
	 * Set the peanuts for this user. Should only ever be called by
	 * endEvent
	 * 
	 * @param newPeanuts WRITEME
	 */
	private void setPeanuts (final BigDecimal newPeanuts) {
		peanuts = newPeanuts;
		changed ();
	}

	/**
	 * @see org.starhope.appius.user.User#setSubclassValues(ResultSet)
	 */
	@Override
	protected void setSubclassValues (final ResultSet resultSet)
			throws SQLException {
		peanuts = resultSet.getBigDecimal ("peanuts");
		tootTimeLeft = resultSet.getInt ("tootTimeLeft");
		tootTimeLeftMinutes = resultSet.getInt ("tootTimeLeftMinutes");
		tootTimeRefill = resultSet.getInt ("tootTimeRefill");
		tootTimerDay = resultSet.getString ("tootTimerType").equals (
				"D");
		tootTimerMonth = !tootTimerDay;

		fetch_house ();
	}

	/**
	 * @param useDailyTimer the tootTimerDay to set
	 */
	public void setTootTimerDay (final boolean useDailyTimer) {
		tootTimerDay = useDailyTimer;
		changed ();
	}

	/**
	 * @param refillAmount the tootTimeRefill to set
	 */
	public void setTootTimeRefill (final int refillAmount) {
		tootTimeRefill = refillAmount;
		changed ();
	}

	/**
	 * @param useMonthlyTimer the tootTimerMonth to set
	 */
	public void setTootTimerMonth (final boolean useMonthlyTimer) {
		// default setter (brpocock, Jul 8, 2009)
		tootTimerMonth = useMonthlyTimer;
		changed ();
	}

	/**
	 * WRITEME: document this method (brpocock, Sep 8, 2009)
	 * 
	 * @param moniker WRITEME
	 */
	public void stampPassport (final String moniker) {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
					.prepareStatement ("INSERT IGNORE INTO userWorlds (userID, worldID) SELECT ?, roomList.ID FROM roomList WHERE moniker=?");
			st.setInt (1, getUserID ());
			st.setString (2, moniker);
			st.executeUpdate ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (
					"trouble stamping passport for " + moniker, 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.user.User#startEnrolment(org.starhope.appius.mb.UserEnrolment)
	 */
	@Override
	public void startEnrolment (final UserEnrolment userEnrolment) {
		final int productID = userEnrolment.getProductID ();
		final String orderSource = userEnrolment.getOrderSource ();
		try {
			if (2 == productID && "auth".equals (orderSource)) {
				addPeanuts (new BigDecimal (5000), "subscription");
			}
			if (3 == productID && "auth".equals (orderSource)) {
				addPeanuts (new BigDecimal (7000), "subscription");
			}
		} catch (final AlreadyExistsException e) {
			// No op.
		}
	}

	/**
	 * <p>
	 * Start an event, and return the results as a partial JSON Object.
	 * </p>
	 * <p>
	 * If the event has already been completed, then there will be an
	 * element "alreadyDone" set to "true". This currently only applies
	 * to wishing fountains, which can only be visited once per day.
	 * </p>
	 * <p>
	 * If the event is started successfully, this will return the event
	 * ID in "eventID".
	 * </p>
	 * <p>
	 * Java-native callers who aren't trying to talk JSON with strangers
	 * can use {@link #startEventRaw(String)}, which this routine is
	 * syntactic sugar around.
	 * </p>
	 * 
	 * @param moniker The event moniker to be created
	 * @return a JSON object, containing one of "eventID" (int) or
	 *         "alreadyDone" (Boolean true)
	 */
	@Override
	public JSONObject startEvent (final String moniker) {
		final JSONObject result = new JSONObject ();
		try {
			try {
				PreparedStatement st = null;

				final int eventID = startEventRaw (moniker);
				result.put ("eventID", eventID);
				Connection con = null;
				ResultSet rs = null;
				try {
					con = AppiusConfig.getDatabaseConnection ();
					st = con
							.prepareStatement ("SELECT * FROM eventTypes WHERE name=?");
					st.setString (1, moniker);
					st.execute ();
					rs = st.getResultSet ();
					rs.next ();
					result.put ("moniker", rs.getString ("name"));
					result.put ("filename", rs.getString ("filename"));
					result
							.put ("asVersion", rs
									.getString ("ASVersion"));
				} 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 */
						}
					}

				}
			} catch (final AlreadyExistsException e) {
				result.put ("alreadyDone", true);
			}
		} catch (final JSONException e) {
			// unforseeably weird edge case.
			AppiusClaudiusCaecus.reportBug (e);
		}
		return result;
	}

	/**
	 * Register a new event. Closes out (cancels) any existing events
	 * for this user (with zero score) as a side-effect.
	 * 
	 * @param moniker The event moniker to register
	 * @return The integer event ID of the new event.
	 * @throws AlreadyExistsException if this is a fountain that has
	 *         already been visited today, and can't be revisited until
	 *         midnight.
	 */
	public int startEventRaw (final String moniker)
			throws AlreadyExistsException {
		int eventID = -1;
		PreparedStatement fountainToday = null;
		PreparedStatement createEvent = null;
		ResultSet newEventInfo = null;
		Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();

			if (moniker.startsWith ("fountain")) {
				fountainToday = con
						.prepareStatement ("SELECT COUNT(*) FROM events WHERE creationTimestamp > DATE(NOW()) AND eventTypeID = (SELECT ID FROM eventTypes WHERE name=?) AND creatorID = ?");
				fountainToday.setString (1, moniker);
				fountainToday.setInt (2, userID);
				fountainToday.execute ();
				final ResultSet canFountain = fountainToday
						.getResultSet ();
				if (canFountain.next ()) {
					final int fountained = canFountain.getInt (1);
					if (fountained > 0)
						// AppiusClaudiusCaecus
						// .reportBug
						// ("Fountain re-use enabled for testing. Fountain visits = "
						// + fountained);
						throw new AlreadyExistsException (moniker);
				}
			}

			/*
			 * clear = conn .prepareStatement(
			 * "UPDATE events SET completionTimestamp=NOW() WHERE completionTimestamp IS NULL AND creatorID=?"
			 * ); clear.setInt (1, userID); clear.executeUpdate ();
			 * clear.close ();
			 */

			con = AppiusConfig.getDatabaseConnection ();
			createEvent = con
					.prepareStatement (
							"INSERT INTO events (creatorID, creationTimestamp, eventTypeID) SELECT ?, NOW(), ID FROM eventTypes WHERE name = ?",
							Statement.RETURN_GENERATED_KEYS);
			createEvent.setInt (1, userID);
			createEvent.setString (2, moniker);

			if (createEvent.executeUpdate () == 0)
				throw AppiusClaudiusCaecus
						.fatalBug ("Event not started: creatorID="
								+ userID + "; moniker=" + moniker);

			newEventInfo = createEvent.getGeneratedKeys ();
			if (newEventInfo.next ()) {
				try {
					eventID = newEventInfo.getInt (1);
				} catch (final SQLException e1) {
					throw AppiusClaudiusCaecus
							.fatalBug (new SQLDataException (
									"Can't retrieve auto-incremented key from “INSERT INTO events”"));
				} finally {
					newEventInfo.close ();
				}
			}
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {

			if (null != newEventInfo) {
				try {
					newEventInfo.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != fountainToday) {
				try {
					fountainToday.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != createEvent) {
				try {
					createEvent.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return eventID;

	}

	/**
	 * Sends a limited subset of data for the client to know.
	 * 
	 * @return A subset of the user record as an ActionScript object
	 */
	@Override
	public JSONObject toJSON () {
		final JSONObject self = new JSONObject ();
		try {
			self.put ("avatar", getPublicInfo ());
			self.put ("isBirthday", isBirthday ());
			self.put ("isMember", isPaidMember ());
			self.put ("canEnterChatZone", isCanEnterChatZone ());
			self.put ("canEnterMenuZone", isCanEnterMenuZone ());
			self.put ("canTalk", isCanTalk ());
			self.put ("language_dialect", language + "_" + dialect);
			self.put ("needsParent", needsParent ()
					&& getParentID () == -1);
			self.put ("tootTimeLeft", "" + tootTimeLeft + ":"
					+ tootTimeLeftMinutes);
			self.put ("tootTimer", tootTimerDay ? "day" : "week");
			self.put ("userID", userID);
			self.put ("userName", getLogin ());
			self.put ("peanuts", peanuts.toPlainString ());
			self.put ("staffLevel", getStaffLevel ());
			if (null != getServerThread ()) {
				self.put ("ipAddress", getIPAddress ());
			}
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		return self;
	}

	/**
	 * TODO: document this method (brpocock, Dec 10, 2009)
	 * 
	 * @param numNuts WRITEME
	 * @param fromUser WRITEME
	 * @param event WRITEME
	 * @throws AlreadyExistsException WRITEME
	 * @throws JSONException WRITEME
	 */
	public void transferPeanuts (final BigDecimal numNuts,
			final Toot fromUser, final String event)
			throws AlreadyExistsException, JSONException {
		giftPeanuts (numNuts, event);
		fromUser.giftPeanuts (numNuts.negate (), event);
	}

}
