/**
 * <p>
 * Copyright © 2010, Bruce-Robert Pocock &amp; Edward Winkelman &amp;
 * Res Interactive, LLC
 * </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 <a
 * href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.
 * </p>
 * 
 * @author brpocock@star-hope.org
 * @author edward.winkelman@gmail.com
 */
package com.tootsville.game;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.AlreadyExistsException;
import org.starhope.appius.except.GameLogicException;
import org.starhope.appius.except.NonSufficientFundsException;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.game.GameEvent;
import org.starhope.appius.game.GameStateFlag;
import org.starhope.appius.game.Zone;
import org.starhope.appius.game.inventory.GenericItemReference;
import org.starhope.appius.geometry.Coord2D;
import org.starhope.appius.geometry.Polygon;
import org.starhope.appius.physica.Geometry;
import org.starhope.appius.room.Room;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.Nomenclator;
import org.starhope.appius.user.events.ActionHandler;
import org.starhope.appius.user.events.ActionMethod;
import org.starhope.appius.user.events.EventRecord;
import org.starhope.appius.user.events.Quaestor;
import org.starhope.appius.util.AppiusConfig;

/**
 * @author edward.winkelman@gmail.com
 */
public class MagicBox extends GameEvent {
	
	/**
	 * Utility class for tracking unique boxes and handling setting and
	 * clearing room variables
	 * 
	 * @author ewinkelman
	 */
	private final class Box {
		/**
		 * The handler for catching the event action when a user
		 * triggers the server event
		 */
		private transient ActionHandler handler;

		/**
		 * Unique ID of the box
		 */
		final private int id;

		/**
		 * Item ID of the box
		 */
		final private int item;

		/**
		 * Room the box is in
		 */
		final private Room room;
		
		/**
		 * Creates a box
		 * 
		 * @param num WRITEME
		 * @param myRoom WRITEME
		 * @param itemID WRITEME
		 */
		public Box (final int num, final Room myRoom, final int itemID) {
			id = num;
			room = myRoom;
			item = itemID;
		}

		/**
		 * WRITEME ewinkelman Oct 16, 2010
		 */
		public void clear () {
			final HashMap <String, String> vars = new HashMap <String, String> ();
			vars.put ("zoneMagicBox" + getID (), null);
			vars.put ("mc~itemMagicBox" + getID (), "magicBox,depart");
			getRoom ().setVariables (vars);
			AppiusClaudiusCaecus.getKalendor ().schedule (
					System.currentTimeMillis () + 5000,
					new Runnable () {

						@Override
						public void run () {
							final HashMap <String, String> myVars = new HashMap <String, String> ();
							myVars
									.put ("itemMagicBox" + getID (),
											null);
							myVars.put ("mc~itemMagicBox" + getID (),
									null);
							getRoom ().setVariables (myVars);
						}
					});
			Quaestor.ignore (handler);
			handler = null;
		}

		/**
		 * @return the id
		 */
		public int getID () {
			return id;
		}

		/**
		 * @return the item
		 */
		public int getItem () {
			return item;
		}

		/**
		 * @return the room
		 */
		public Room getRoom () {
			return room;
		}
		
		/**
		 * Places the box at a point
		 * 
		 * @param point WRITEME
		 */
		public void place (final Coord2D point) {
			final Coord2D ul = new Coord2D (point.getX () - 15, point
					.getY () - 15);
			final Coord2D ur = new Coord2D (point.getX () + 15, point
					.getY () - 15);
			final Coord2D ll = new Coord2D (point.getX () - 15, point
					.getY () + 15);
			final Coord2D lr = new Coord2D (point.getX () + 15, point
					.getY () + 15);
			handler = new ActionHandler (getRoom (), null,
					"event.srv.enter", null,
					"evt_$magicBox" + getID (), new ActionMethod () {

						@Override
						public boolean acceptAction (final Room where,
								final AbstractUser subject,
								final String verb,
								final AbstractUser object,
								final String indirectObject,
								final Object... trailer) {
							if (subject.isPaidMember ()) {
								putBox ();
								giveReward (subject, getItem ());
							} else {
								subject.acceptSuccessReply ("showVIT",
										new JSONObject (), where);
							}
							return false;
						}
					});
			final Polygon mPolygon = new Polygon (ul, ur, lr, ll);
			Quaestor.listen (handler);
			final HashMap <String, String> vars = new HashMap <String, String> ();
			vars.put ("zoneMagicBox" + getID (), "evt_$magicBox"
					+ getID () + ":" + mPolygon.toRoomVar ());
			vars.put ("itemMagicBox" + getID (), getItem () + "~"
					+ point.getX () + "~" + point.getY () + "~" + "S");
			getRoom ().setVariables (vars);
		}

	}

	/**
	 *
	 */
	private static final AtomicInteger nextInstanceID = new AtomicInteger ();

	/**
	 * Current box
	 */
	private transient Box curBox;

	/**
	 * Time a box was last placed
	 */
	private transient long lastPlaced = 0;

	/**
	 * Magic Box data room object
	 */
	private transient final Room mbRoom;

	/**
	 * This instance's zone
	 */
	private transient final Zone zone;
	
	/**
	 * WRITEME edward.winkelman@gmail.com Sep 28, 2010
	 * 
	 * @param z Zone in which haunted organ is placed
	 * @throws NotFoundException WRITEME
	 * @throws GameLogicException WRITEME
	 */
	public MagicBox (final Zone z) throws NotFoundException,
			GameLogicException {
		super (z, 'S');

		gameState = GameStateFlag.GAME_SOLO;
		mbRoom = z.getRoom ("magicBox");
		if (null == mbRoom) {
			AppiusClaudiusCaecus
					.reportBug ("can't find magic box data room");
			throw new GameLogicException (
					"Can't find magic box data room", this, z);
		}
		rooms.add (mbRoom);
		zone = z;

		AppiusClaudiusCaecus.add (this);
	}

	/**
	 * @see org.starhope.appius.room.RoomListener#acceptGameAction(org.starhope.appius.user.AbstractUser,
	 *      org.json.JSONObject)
	 */
	@Override
	public void acceptGameAction (final AbstractUser u,
			final JSONObject action) {
		// No op

	}

	/**
	 * @see org.starhope.appius.room.RoomListener#acceptOutOfBandMessage(org.starhope.appius.user.AbstractUser,
	 *      org.starhope.appius.room.Room, org.json.JSONObject)
	 */
	@Override
	public void acceptOutOfBandMessage (final AbstractUser sender,
			final Room room, final JSONObject body) {
		// No op

	}

	/**
	 * @see org.starhope.appius.room.RoomListener#acceptPublicMessage(org.starhope.appius.user.AbstractUser,
	 *      java.lang.String)
	 */
	@Override
	public void acceptPublicMessage (final AbstractUser from,
			final String message) {
		// No op

	}

	/**
	 * @see org.starhope.appius.room.RoomListener#acceptUserAction(org.starhope.appius.room.Room,
	 *      org.starhope.appius.user.AbstractUser)
	 */
	@Override
	public void acceptUserAction (final Room r, final AbstractUser u) {
		// No op
	}

	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 */
	private void clearBox () {
		if (curBox != null) {
			curBox.clear ();
		}
		curBox = null;
	}

	/**
	 * @see org.starhope.appius.game.GameEvent#destroySelf()
	 */
	@Override
	public void destroySelf () {
		super.destroySelf ();
	}
	
	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 * 
	 * @return WRITEME
	 * @throws NotFoundException WRITEME
	 */
	private int getABox () throws NotFoundException {
		final String boxStr = mbRoom.getVariable ("data_$boxes");
		final String [] boxes = boxStr.split (",");
		if (boxes.length == 0) {
			throw new NotFoundException (
					"No data_$boxes found in magicBox room");
		}

		int totalWeights = 0;
		for (final String boxe : boxes) {
			totalWeights += getBoxWeight (boxe);
		}
		final int [] idArr = new int [totalWeights];
		int index = 0;
		for (final String boxe : boxes) {
			final int weight = getBoxWeight (boxe);
			final int id = getBoxID (boxe);
			for (int j = 0; j < weight; j++ ) {
				idArr [index++ ] = id;
			}
		}

		return idArr [AppiusConfig.getRandomInt (0, idArr.length - 1)];
	}
	
	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 * 
	 * @param str WRITEME
	 * @return WRITEME
	 */
	private int getBoxID (final String str) {
		final String [] sa = str.split (":");
		return sa.length == 2 ? Integer.parseInt (sa [0]) : -1;
	}
	
	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 * 
	 * @return WRITEME
	 * @throws NotFoundException WRITEME
	 */
	private Room getBoxRoom () throws NotFoundException {
		final String roomStr = mbRoom.getVariable ("data_$rooms");
		final String [] boxRooms = roomStr.split (",");
		if (boxRooms.length == 0) {
			throw new NotFoundException (
					"No rooms found in which to spawn a magic box");
		}
		final String moniker = boxRooms [AppiusConfig.getRandomInt (0,
				boxRooms.length - 1)];
		return zone.getRoom (moniker.trim ());
	}
	
	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 * 
	 * @param str WRITEME
	 * @return WRITEME
	 */
	private int getBoxWeight (final String str) {
		final String [] sa = str.split (":");
		return sa.length == 2 ? Integer.parseInt (sa [1]) : -1;
	}

	/**
	 * @see org.starhope.appius.game.GameEvent#getGameEventPrefix()
	 */
	@Override
	public String getGameEventPrefix () {
		return "magicBox";
	}

	/**
	 * @see org.starhope.appius.util.HasName#getName()
	 */
	@Override
	public String getName () {
		return "Magic Box in " + rooms.first ().getZone ().getName ();
	}
	
	/**
	 * Gets a spawn point for the magic box in the room
	 * 
	 * @param room WRITEME
	 * @return WRITEME
	 * @throws NotFoundException WRITEME
	 */
	private Coord2D getPointInRoom (final Room room)
			throws NotFoundException {
		final LinkedList <String> points = new LinkedList <String> ();
		for (final Map.Entry <String, String> kvp : room
				.getVariables ().entrySet ()) {
			if (kvp.getValue ().startsWith ("evt_$mbSpot")) {
				points.add (kvp.getValue ().substring (0,
						kvp.getValue ().indexOf (":")));
			}
		}
		if (points.size () == 0) {
			throw new NotFoundException (
					"Couldn't find spawn point for magic box in room "
							+ room.getMoniker ());
		}
		final String polyString = room
				.getPlaceStringByName (points.get (AppiusConfig
						.getRandomInt (0, points.size () - 1)));

		return Geometry.stringToNewPolygon (polyString).getCenter ();
	}
	
	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 * 
	 * @param user WRITEME
	 * @param itemID WRITEME
	 */
	void giveReward (final AbstractUser user, final int itemID) {
		final String rewardStr = mbRoom.getVariable ("data_$magicBox"
				+ itemID);
		final String [] rewards = rewardStr.split (",");
		final String [][] rewardArray = new String [rewards.length] [];
		int totalWeight = 0;
		for (int i = 0; i < rewards.length; i++ ) {
			final String [] rewardParms = rewards [i].split (":");
			if (rewardParms.length < 3 || rewardParms.length > 4) {
				continue;
			}
			rewardArray [i] = rewardParms;
			totalWeight += Integer.parseInt (rewardArray [i] [2]);
		}

		int rewardNum = AppiusConfig.getRandomInt (1, totalWeight);
		for (int i = 0; i < rewardArray.length && rewardNum > 0; i++ ) {
			rewardNum -= Integer.parseInt (rewardArray [i] [2]);
			if (rewardNum < 1) {
				if (rewardArray [i].length == 4) {
					final JSONObject soundKey = new JSONObject ();
					try {
						soundKey.put ("url", rewardArray [i] [3]);
					} catch (final JSONException e) {
						AppiusClaudiusCaecus
								.reportBug (
										"Caught a JSONException in MagicBox/playSound",
										e);
					}
					user.getRoom ().broadcast ("playSound", soundKey);
				}
				if (rewardArray [i] [0].equals ("i")) {
					rewardWithItem (user, Integer
							.parseInt (rewardArray [i] [1]));
				} else {
					rewardWithPeanuts (user, Integer
							.parseInt (rewardArray [i] [1]));
				}
			}
		}
	}

	/**
	 * WRITEME ewinkelman Oct 16, 2010
	 */
	synchronized void putBox () {
		clearBox ();
		try {
			final Room newRoom = getBoxRoom ();
			final Coord2D point = getPointInRoom (newRoom);
			final int itemID = getABox ();
			curBox = new Box (MagicBox.nextInstanceID
					.getAndIncrement (), newRoom, itemID);
			curBox.place (point);
			lastPlaced = System.currentTimeMillis ();
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}
	
	/**
	 * Give the user the specified item as a reward.
	 * 
	 * @param user The user receiving the reward
	 * @param itemID The item ID to give to the user
	 */
	private void rewardWithItem (final AbstractUser user,
			final int itemID) {
		try {
			final EventRecord eventRecord = Quaestor.startEvent (user,
					"magicBox");
			eventRecord.end (Nomenclator.getDataRecord (
					GenericItemReference.class, itemID),
					BigDecimal.ZERO);
		} catch (final AlreadyExistsException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final NonSufficientFundsException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		
		if (Quaestor.getItemGainedEventCount (user, "magicBox", itemID) == 0) {
			AppiusClaudiusCaecus.reportBug ("Wait, I just gave "
					+ user.getDebugName () + " an item " + itemID
					+ " and s/he lost it?");
		}
	}
	
	/**
	 * WRITEME ewinkleman Oct 16, 2010
	 * 
	 * @param user WRITEME
	 * @param value WRITEME
	 */
	private void rewardWithPeanuts (final AbstractUser user,
			final int value) {
		try {
			final EventRecord eventRecord = Quaestor.startEvent (user,
					"magicBox");
			eventRecord.end (BigInteger.valueOf (value));
		} catch (final AlreadyExistsException e) {
			// TODO Auto-generated catch block
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final NotFoundException e) {
			// TODO Auto-generated catch block
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * @see org.starhope.appius.game.GameEvent#tick(long, long)
	 */
	@Override
	public void tick (final long currentTime, final long deltaTime)
			throws UserDeadException {
		if (lastPlaced
				+ AppiusConfig.getIntOrDefault (
						"com.tootsville.game.MagicBox.period", 300000) < currentTime) {
			putBox ();
		}
	}

}
