/**
 * Copyright © 2010, Res Interactive, LLC. All Rights Reserved.
 */
package com.tootsville.npc;

import java.awt.geom.GeneralPath;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import org.starhope.appius.except.GameLogicException;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.except.PrivilegeRequiredException;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.game.Zone;
import org.starhope.appius.geometry.Coord3D;
import org.starhope.appius.geometry.Polygon;
import org.starhope.appius.physica.Geometry;
import org.starhope.appius.room.Room;
import org.starhope.appius.user.AbstractNonPlayerCharacter;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.Nomenclator;
import org.starhope.appius.user.PathFinder;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;
import org.starhope.util.types.Pair;

/**
 * @author brpocock@star-hope.org
 */
public class Smudge extends AbstractNonPlayerCharacter {

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	final class DoSomethingRunner implements Runnable {
		/**
		 * @see Runnable#run()
		 */
		@Override
		public void run () {
			doSomething ();
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	final class MoveRandomlyRunner implements Runnable {
		/**
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run () {
			moveRandomly ();
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	final class RunBesmudgement implements Runnable {
		/**
		 * @see Runnable#run()
		 */
		@Override
		public void run () {
			besmudge ();
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	static final class SmudgeGoHome implements Runnable {
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		private final Smudge smudge;

		/**
		 * WRITEME: Document this constructor brpocock@star-hope.org
		 *
		 * @param aSmudge WRITEME
		 */
		SmudgeGoHome (final Smudge aSmudge) {
			smudge = aSmudge;
		}

		/**
		 * @see Runnable#run()
		 */
		@Override
		public void run () {
			smudge.scheduleNextPatrol ();
			Room nowhere;
			try {
				nowhere = smudge.getRoom ().getZone ().getRoomByName (
				"nowhere");
			} catch (final NotFoundException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a NotFoundException in SmudgeGoHome.run finding nowhere ",
						e);
				return;
			}
			nowhere.join (smudge);
			smudge.setRoom (nowhere);
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	final class StainDropRunner implements Runnable {
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		private final AbstractNonPlayerCharacter smudge;

		/**
		 * WRITEME: Document this constructor brpocock@star-hope.org
		 *
		 * @param aSmudge the NPC
		 */
		StainDropRunner (final AbstractNonPlayerCharacter aSmudge) {
			smudge = aSmudge;
		}

		/**
		 * @see Runnable#run()
		 */
		@Override
		public void run () {
			smudge.speakCasually ("say");

			// ----------------
			new SmudgeStain (getRoom (), getLocation ());
			// ----------------
			AppiusClaudiusCaecus.getKalendor ().schedule (
					System.currentTimeMillis () + 5000,
					new WanderRunner ());
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	final class SurfaceOntoBeachRunner implements Runnable {
		/**
		 * re-appear
		 */
		@Override
		public void run () {
			if (AppiusConfig.getRandomInt (0, 3) == 1) {
				startRoom = "shadowLandEntrance";
			}
			startRoom = "bigTootoonaVolleyball";
			surfaceOntoBeach ();
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	private final class TravelToRoomRunner implements Runnable {
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		private final String roomToward;
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		private final AbstractNonPlayerCharacter smudge;

		/**
		 * WRITEME: Document this constructor brpocock@star-hope.org
		 *
		 * @param towardWhichRoom WRITEME
		 * @param aSmudge WRITEME
		 */
		TravelToRoomRunner (final String towardWhichRoom,
				final AbstractNonPlayerCharacter aSmudge) {
			roomToward = towardWhichRoom;
			smudge = aSmudge;
		}

		/**
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run () {
			Room newRoom;
			try {
				newRoom = getZone ().getRoomByName (roomToward);
			} catch (final NotFoundException e) {
				AppiusClaudiusCaecus.reportBug (
						"Caught a NotFoundException in room link to "
						+ roomToward, e);
				doSomething ();
				return;
			}
			newRoom.join (smudge);
			setRoom (newRoom);
			doSomething ();
		}
	}

	/**
	 * WRITEME: Document this type.
	 *
	 * @author brpocock@star-hope.org
	 */
	final class WanderRunner implements Runnable {
		/**
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run () {
			wander ();

		}
	}

	/**
	 * WRITEME
	 */
	private final static int GREETING_RATE = 20 * 60000;

	/**
	 * Counter for getting instance ID's
	 */
	protected final static AtomicInteger instanceCounter = new AtomicInteger ();

	/**
	 * stop staining the room when we hit this point
	 */
	private final static int MAX_SMUDGES_PER_ROOM = 10;

	/**
	 * WRITEME
	 */
	private final static int OBSERVATION_RATE = 1000;

	/**
	 * Java serialisation unique ID
	 */
	private static final long serialVersionUID = 7197433632861194562L;
	/**
	 * WRITEME
	 */
	private boolean fleeing = false;

	/**
	 * WRITEME
	 */
	final private Pattern hiPattern = Pattern
	.compile (
			"(hi|hola|howdy|hello|yo|hey|allo|bonjour|bonsoir|buenos dias|buenos noches|welcome|benvenido|bem vindo|holla back|bienvenue)",
			Pattern.CASE_INSENSITIVE);

	/**
	 * WRITEME
	 */
	private long lastObservedPlayers = 0;

	/**
	 * WRITEME
	 */
	private boolean returningToOcean = false;

	/**
	 * WRITEME
	 */
	private long seenShade = 0;

	/**
	 * WRITEME
	 */
	private long seenZap = 0;

	/**
	 * Room items, the things that get left behind when Smudge smudges
	 * up a place.
	 */
	final List <Integer> smudgeItems = new LinkedList <Integer> ();

	/**
	 * WRITEME: Document this brpocock@star-hope.org
	 */
	public String startRoom = "bigTootoonaVolleyball";
	/**
	 * frequency to check for future actions
	 */
	long tLastActionCheck = 0;

	/**
	 * WRITEME brpocock@star-hope.org
	 *
	 * @param z zone in which to spawn
	 * @throws GameLogicException WRITEME
	 * @throws NotFoundException WRITEME
	 */
	public Smudge (final Zone z) throws NotFoundException,
	GameLogicException {
		super ("Smudge");
		currentRoom = z.getRoomByName (startRoom);
		currentRoom.join (this);
		getSmudgeItems ();
		surfaceOntoBeach ();
	}

	/**
	 * @see org.starhope.appius.user.AbstractNonPlayerCharacter#acceptPublicMessage(org.starhope.appius.user.AbstractUser,
	 *      java.lang.String)
	 */
	@Override
	public void acceptPublicMessage (final AbstractUser from,
			final String message) {
		if (from.equals (this)) return;
		checkForLazySmudge (System.currentTimeMillis ());
		final String lcase = message.toLowerCase (Locale.ENGLISH);
		if (hiPattern.matcher (message).matches ()
				&& lcase.contains ("smudge")) {
			speakCasually ("hi");
			return;
		}
		if (lcase.contains ("where")
				&& lcase.indexOf ("shade") > lcase.indexOf ("where")) {
			final AbstractUser shade = Nomenclator
			.getUserByLogin ("Shade");
			if (shade.isOnline ()) {
				speakCasually ("wheresShade.present");
			} else {
				speakCasually ("wheresShade.absent");
			}
			return;
		}
		if (lcase.contains ("shade") || lcase.contains ("shadow")
				|| lcase.contains ("shaddow")) {
			speakCasually ("hearShade");
			return;
		}
		if (lcase.equals ("/peace")) {
			speakCasually ("peace");
			return;
		}
		if (lcase.equals ("/zap")) {
			speakCasually ("zapEmote");
			return;
		}
	}

	/**
	 * @return true, if all rooms have max smudges
	 */
	private boolean allRoomsAreSmudgedUp () {
		final int maxSmudges = getMaxSmudges ();
		for (final String moniker : getAccessibleRooms ()) {
			Room r;
			try {
				r = getZone ().getRoomByName (moniker);
			} catch (final NotFoundException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a NotFoundException in Smudge.allRoomsAreSmudgedUp ",
						e);
				return true;
			}
			if (maxSmudges > countSmudges (r)) return false;
		}
		return true;
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#ban(org.starhope.appius.user.AbstractUser,
	 *      java.lang.String)
	 */
	@Override
	public void ban (final AbstractUser u, final String banReason)
	throws PrivilegeRequiredException {
		destroy ();
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	void besmudge () {
		// TODO: circle the spot …
		// FIXME: Moves south 5 pixels due to client bug causing him to
		// drift randomly
		getRoom ().goTo (this, getLocation ().add (0, 5, 0), "N",
		"SlimeDrop");
		final AbstractNonPlayerCharacter smudge = this;
		kalendor.schedule (System.currentTimeMillis () + 3000,
				new StainDropRunner (smudge));
	}

	/**
	 * @param name writeme
	 * @return WRITEME
	 */
	public boolean canSeeUser (final String name) {
		for (final AbstractUser u : currentRoom.getAllUsers ()) {
			if (u.getAvatarLabel ().equals (name)
					|| u.getAvatarLabel ().startsWith (name + "$")) return true;
		}
		return false;
	}

	/**
	 * See if this Smudge has become lazy, and lost his way; if so,
	 * start him rocking again
	 *
	 * @param currentTime the current time
	 */
	private void checkForLazySmudge (final long currentTime) {
		if (getRoom ().isLimbo ()) return;

		if (tLastActionCheck < currentTime - 10000) {
			final Map <Long, Runnable> schedule = kalendor
			.getMySchedule ();

			// don't consider things happening more than a minute into
			// the future
			final Iterator <Entry <Long, Runnable>> i = schedule
			.entrySet ().iterator ();
			while (i.hasNext ()) {
				final Entry <Long, Runnable> activity = i.next ();
				if (activity.getKey ().longValue () > currentTime + 60000) {
					i.remove ();
				}
			}

			// if we're not doing anything “soon,” well, do something…
			if (schedule.size () == 0) {
				AppiusClaudiusCaecus
				.reportBug ("Smudge got lazy, but I've lit a fire under him. (It doesn't smell very good.)");
				doSomething ();
			}
			tLastActionCheck = currentTime;
		}
	}

	/**
	 * @param currentTime the time it's called
	 */
	private void checkObservedPlayers (final long currentTime) {
		if (lastObservedPlayers < currentTime - Smudge.OBSERVATION_RATE) {
			if (seenShade < currentTime - Smudge.GREETING_RATE) {
				if (canSeeUser ("shade")) {
					speakCasually ("seeShade");
					seenShade = currentTime;
				}
			}
			if (seenZap < currentTime - Smudge.GREETING_RATE) {
				if (canSeeUser ("zap")) {
					speakCasually ("seeZap");
					fleeing = true;
					seenZap = currentTime;
				}
			}
			lastObservedPlayers = currentTime;
		}
	}

	/**
	 * count the number of smudges present in a given room
	 *
	 * @param r the room
	 * @return the number of smudges
	 */
	private int countSmudges (final Room r) {
		int c = 0;
		for (final Map.Entry <String, String> var : r.getVariables ()
				.entrySet ()) {
			if ( !var.getKey ().startsWith ("zone")) {
				continue;
			}
			if (var.getValue ().startsWith ("evt_$SmudgeStain/")) {
				++c;
			}
		}
		return c;
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	public void doSomething () {
		if (fleeing) {
			moveTowardsExit ();
		}

		switch (AppiusConfig.getRandomInt (0, 4)) {
			case 0:
			case 1:
			case 2:
				// do nothing, linger.
				AppiusClaudiusCaecus.getKalendor ().schedule (
						System.currentTimeMillis () + 5000,
						new DoSomethingRunner ());
				return;
			case 3:
				// heckle
				speakCasually ("say");
				return;
			default:
				// continue; do something more important
		}

		if (returningToOcean) {
			moveTowardsExit ( ((PathFinder) pathFinder)
					.getRoomToward ("tootoonaBeachVolleyball"));
			return;
		}
		if (roomIsSmudgedUp ()) {
			if (allRoomsAreSmudgedUp ()) {
				returnToOcean ();
				return;
			}
			moveTowardsExit ();
		}

		// smudge up the room
		pickASpotAndSmudgeIt ();

	}

	/**
	 * @see org.starhope.appius.user.AbstractNonPlayerCharacter#getInstanceID()
	 */
	@Override
	protected int getInstanceID () {
		return Smudge.instanceCounter.incrementAndGet ();
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 *
	 * @return WRITEME
	 */
	private int getMaxSmudges () {
		return AppiusConfig.getIntOrDefault (
				"com.tootsville.npc.Smudge.maxSmudges", 10);

	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	private void getSmudgeItems () {
		for (final String itemAsString : AppiusConfig
				.getList ("com.tootsville.npc.Smudge.smudgeItems")) {
			smudgeItems.add (Integer.valueOf (itemAsString));
		}
		if (smudgeItems.size () == 0) {
			smudgeItems.add (Integer.valueOf (2419));
		}
	}

	/**
	 * @see org.starhope.appius.user.AbstractNonPlayerCharacter#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc.makeHashCode (toString ());
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	public void moveRandomly () {
		for (int i = 0; i < 50; ++i) {
			final Coord3D there = Coord3D.randomIn (currentRoom);
			if (currentRoom.canWalk (this, there)) {
				currentRoom.goTo (this, there, null, "Walk");
				whenAtTarget (new DoSomethingRunner ());
				return;
			}
		}
		kalendor.schedule (System.currentTimeMillis () + 300,
				new MoveRandomlyRunner ());
		return;
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	private void moveTowardsExit () {
		int randomExit = AppiusConfig.getRandomInt (0, currentRoom
				.getExits ().size () - 1);
		final Iterator <Entry <String, Pair <String, Polygon>>> i = currentRoom
		.getExits ().entrySet ().iterator ();
		while (randomExit-- > 0) {
			i.next ();
		}
		moveTowardsExit (i.next ().getValue ().head ());
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 *
	 * @param roomToward WRITEME
	 */
	private void moveTowardsExit (final String roomToward) {
		if (currentRoom.isLimbo ()) return;
		try {
			final Coord3D exitPoint = Geometry.getRandomPointWithin (
					currentRoom.getExitTo (roomToward)).toCoord3D ();
			if (currentRoom.canWalk (this, exitPoint)) {
				currentRoom.goTo (this, exitPoint, null, "Walk");
				final AbstractNonPlayerCharacter smudge = this;
				whenAtTarget (new TravelToRoomRunner (roomToward,
						smudge));
				return;
			}
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus
			.reportBug (
					"Caught a NotFoundException in Smudge.moveTowardsExit ",
					e);
			// fall through
		}
		moveRandomly ();
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	private void pickASpotAndSmudgeIt () {
		currentRoom.goTo (this, currentRoom
				.findPointWithin (currentRoom.getWalkableSpace ()),
				null, "Walk");
		whenAtTarget (new RunBesmudgement ());
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	private void returnToOcean () {
		returningToOcean = true;
		if (currentRoom.getMoniker ().equals ("bigTootoonaVolleyball")) {
			currentRoom.goTo (this, new Coord3D (200, 200, 0), null,
			"Walk");
			whenAtTarget (new SmudgeGoHome (this));
			return;
		}
		if (currentRoom.getMoniker ().equals ("shadowLandsEntrance")) {
			currentRoom.goTo (this, new Coord3D (400, 400, 0), null,
			"Walk");
			whenAtTarget (new SmudgeGoHome (this));
			return;
		}
		moveTowardsExit (startRoom);
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 *
	 * @return true, if the room is all smudged up already
	 */
	private boolean roomIsSmudgedUp () {
		return countSmudges (currentRoom) > Smudge.MAX_SMUDGES_PER_ROOM;
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	void scheduleNextPatrol () {
		kalendor.schedule (System.currentTimeMillis () + 60000 * 13,
				new SurfaceOntoBeachRunner ());
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	void surfaceOntoBeach () {
		fleeing = false;
		returningToOcean = false;
		Room beach;
		if (null == currentRoom.getZone ()) {
			destroy ();
			return;
		}
		try {
			beach = getZone ().getRoomByName (startRoom);
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus
			.reportBug (
					"Caught a NotFoundException in Smudge.surfaceOntoBeach ",
					e);
			return;
		}
		beach.join (this, "user~smudge~0");
		setRoom (beach);
		setLocation (new Coord3D (200, 200, 0));
		kalendor.schedule (System.currentTimeMillis () + 15000,
				new WanderRunner ());
	}

	/**
	 * @see org.starhope.appius.util.AcceptsMetronomeTicks#tick(long,
	 *      long)
	 */
	@Override
	public void tick (final long currentTime, final long deltaTime)
	throws UserDeadException {
		checkObservedPlayers (currentTime);
		// checkForLazySmudge (currentTime);
		super.tick (currentTime, deltaTime);
	}

	/**
	 * WRITEME: Document this method brpocock@star-hope.org
	 */
	public void wander () {
		final GeneralPath floorspace = currentRoom.getWalkableSpace ();
		for (int limit = 50; limit > 0; --limit) {
			final Coord3D dart = Coord3D.randomIn (currentRoom);
			if (floorspace.contains (dart.getX (), dart.getY ())) {
				currentRoom.goTo (this, dart, null, "Walk");
				whenAtTarget (new DoSomethingRunner ());
				return;
			}
		}
		AppiusClaudiusCaecus.getKalendor ().schedule (
				System.currentTimeMillis () + 1234,
				new DoSomethingRunner ());
	}

}
