/**
 * <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.util.HashMap;
import java.util.Map;

import org.json.JSONObject;
import org.starhope.appius.except.GameLogicException;
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.PhysicsScheduler;
import org.starhope.appius.game.Zone;
import org.starhope.appius.geometry.Coord2D;
import org.starhope.appius.geometry.Coord3D;
import org.starhope.appius.geometry.LineSeg2D;
import org.starhope.appius.geometry.Polygon;
import org.starhope.appius.geometry.Vector2D;
import org.starhope.appius.physica.Collidable;
import org.starhope.appius.physica.CollisionObject;
import org.starhope.appius.physica.Geometry;
import org.starhope.appius.physica.RigidBody;
import org.starhope.appius.room.Room;
import org.starhope.appius.user.AbstractNonPlayerCharacter;
import org.starhope.appius.user.AbstractUser;
import org.starhope.util.LibMisc;

import com.tootsville.npc.SoccerBall;
import com.tootsville.npc.Stu;

/**
 * @author brpocock@star-hope.org
 * @author ewinkelman
 */
public class SoccerField extends GameEvent {
	
	/**
	 * Just for convenience
	 * 
	 * @author ewinkelman
	 */
	private enum RefPhrases {
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		gameOn,
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		newGame,
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		score,
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		outOfBounds,
		/**
		 * WRITEME: Document this brpocock@star-hope.org
		 */
		goal
	}

	/**
	 * Serialization ID
	 */
	private static final long serialVersionUID = -4854367299114025393L;

	/**
	 * Pointer to the user that represents the soccer ball
	 */
	private transient SoccerBall ball;

	/**
	 * Center of the field
	 */
	private transient final Coord2D centerField;

	/**
	 * Exit points when the soccer ball goes out of bounds
	 */
	private transient Coord2D entryPoint;

	/**
	 * The polygon that contains the dimensions of the field
	 */
	private final transient Polygon field;

	/**
	 * Boolean to freeze the ball from colliding
	 */
	private boolean outOfBounds = false;

	/**
	 * the left-side goal region
	 */
	private final transient Polygon goalLeft;

	/**
	 * the right-side goal region
	 */
	private final transient Polygon goalRight;

	/**
	 * The last time the soccer ball moved
	 */
	private long lastTimeMoving = System.currentTimeMillis ();

	/**
	 * Referee character
	 */
	private transient AbstractNonPlayerCharacter referee;

	/**
	 * Soccer field room object
	 */
	private transient final Room soccerField;

	/**
	 * Is the ball in the left goal?
	 */
	private boolean inGoal;

	/**
	 * Score for the left side team
	 */
	private int leftScore = 0;

	/**
	 * Score for the right side team
	 */
	private int rightScore = 0;

	/**
	 * Last time a goal was made
	 */
	private long lastGoalTime = System.currentTimeMillis ();

	/**
	 * @param z zone
	 * @throws GameLogicException WRITEME
	 * @throws NotFoundException WRITEME
	 */
	public SoccerField (final Zone z) throws GameLogicException,
			NotFoundException {
		super (z, 'S');
		gameState = GameStateFlag.GAME_SOLO;
        soccerField = z.getRoom ("soccerField");
		if (null == soccerField) {
			AppiusClaudiusCaecus
					.reportBug ("can't find the soccer field");
			throw new GameLogicException (
					"Can't find the soccer field", this, z);
		}
		rooms.add (soccerField);
		soccerField.add (this);

		Polygon temp = Geometry.stringToNewPolygon (soccerField
				.getPlaceStringByName ("evt_$field"));
		field = null == temp || temp.getPoints ().length < 4 ? new Polygon (
				new Coord2D (134, 197), new Coord2D (668, 196),
				new Coord2D (775, 494), new Coord2D (32, 492))
				: temp;
		centerField = field.getBoundingCircle ().getCenter ();

		temp = Geometry.stringToNewPolygon (soccerField
				.getPlaceStringByName ("evt_$goalLeft"));
		goalLeft = null == temp || temp.getPoints ().length < 4 ? new Polygon (
				new Coord2D (0, 0), new Coord2D (0, 100), new Coord2D (
						100, 100), new Coord2D (100, 0))
				: temp;
		temp = Geometry.stringToNewPolygon (soccerField
				.getPlaceStringByName ("evt_$goalRight"));
		goalRight = null == temp || temp.getPoints ().length < 4 ? new Polygon (
				new Coord2D (700, 0), new Coord2D (700, 100),
				new Coord2D (800, 100), new Coord2D (800, 0))
				: temp;
		ball = new SoccerBall ();
		ball.setTravelRate (0d);
		referee = new Stu ();
		soccerField.join (ball);
		soccerField.join (referee);
		soccerField.putHere (ball, centerField.toCoord3D ());
		soccerField.putHere (referee, new Coord3D (400d, 175d, 0d));

		PhysicsScheduler.start (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
	}

	/**
	 * Cleanup routine to make sure we get garbage collected properly
	 */
	@Override
	public void destroySelf () {
		super.destroySelf ();
		PhysicsScheduler.stop (this);
        if (null != ball) {
			soccerField.part (ball);
		}
        if (null != referee) {
			soccerField.part (referee);
		}
		ball.destroy ();
		referee.destroy ();
		ball = null;
		referee = null;
	}

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

	/**
	 * @see org.starhope.appius.util.HasName#getName()
	 */
	@Override
	public String getName () {
		return "Soccer Field in "
				+ rooms.first ().getZone ().getName ();
	}

	/**
	 * Mostly because I wanted to make sure Stu isn't spamming speech
	 * when no one is around to hear it
	 *
	 * @param phrase phrase key
	 */
	private void speakRefSpeak (final RefPhrases phrase) {
		if (soccerField.getAllUsers ().size () > 2) {
			String sayWhat = "";
			switch (phrase) {
			case gameOn:
				sayWhat = LibMisc.getText ("npc.Stu.game_on");
				break;
			case newGame:
				sayWhat = LibMisc.getText ("npc.Stu.new_game");
				break;
			case score:
				sayWhat = String.format (LibMisc
						.getText ("npc.Stu.current_score"), Integer
						.valueOf (leftScore), Integer
						.valueOf (rightScore));
				break;
			case outOfBounds:
				sayWhat = LibMisc.getText ("npc.Stu.out_of_bounds");
				break;
			case goal:
				sayWhat = LibMisc.getText ("npc.Stu.goal");
				break;
			default:
				// no op
			}
			if (null != sayWhat && !"".equals (sayWhat)) {
				referee.speak (soccerField, sayWhat);
			}
		}
	}

	/**
	 * @see org.starhope.appius.game.GameEvent#tick(long, long)
	 */
	@Override
	public void tick (final long currentTime, final long deltaTime)
			throws UserDeadException {
		super.tick (currentTime, deltaTime);
		assert !Double.isNaN (ball.getTravelRate ()) : "Soccer ball travel rate is NaN";
		// Decelerate at 50 pixels per second per second to a minimum of
		// 0
		final double newTravelRate = Math.max (ball.getTravelRate ()
				- 0.05 * deltaTime, 0);
		if (Math.abs (newTravelRate - ball.getTravelRate ()) > .000001) {
			ball.setTravelRate (newTravelRate);
		}

        if ( !outOfBounds) {
            // Populate hash maps with collidable objects and their
            // clones
            // XXX More efficient if we track users as they join/leave
            // the
            // room and create/destroy/update clones appropriately
            final HashMap <Collidable, CollisionObject> entities = new HashMap <Collidable, CollisionObject> ();
            try {
                ball.getLocationForUpdate (); // Lock ball location
                final CollisionObject ballCollider = new CollisionObject (
                        ball, currentTime);
                for (final AbstractUser abstractUser : soccerField
                        .getAllUsers ()) {
                    if (abstractUser == ball || abstractUser == referee) {
                        continue;
                    }
                    if (abstractUser instanceof Collidable) {
                        // Lock collidable object
                        abstractUser.getLocationForUpdate ();
                        final Collidable collidable = (Collidable) abstractUser;
                        final CollisionObject collisionObject = new CollisionObject (
                                collidable, currentTime);
                        collisionObject.move (currentTime
                                - abstractUser.getTravelStart ());
                        entities.put (collidable, collisionObject);
                    }
                }
                
                for (final Map.Entry <Collidable, CollisionObject> entry : entities
                        .entrySet ()) {
                    final Collidable collidableUser = entry.getKey ();
                    final CollisionObject collidable = entry
                            .getValue ();
                    
                    try {
                        RigidBody.collide (ballCollider, collidable,
                                currentTime);
                    } catch (final GameLogicException e) {
                        final String name = collidableUser instanceof AbstractUser ? ((AbstractUser) collidableUser)
                                .getDebugName ()
                                : "";
                        AppiusClaudiusCaecus
                                .blather (
                                        name,
                                        soccerField.getDebugName (),
                                        "",
                                        "User failed to find collision with ball in last 100ms",
                                        true);
                    }
                    
                    if (collidable.changed ()) {
                        collidableUser.setCenterOfMass (collidable
                                .getCenterOfMass ());
                        collidableUser.setVelocity (collidable
                                .getVelocity ());
                    }
                }
                if (ballCollider.changed ()) {
                    ball.setCenterOfMass (ballCollider
                            .getCenterOfMass ());
                    ball.setVelocity (ballCollider.getVelocity ());
                }
            } finally {
                try {
                    ball.unlockLocation ();
                } catch (final IllegalMonitorStateException e) {
                    // eat it
                }
                for (final Collidable abstractUser : entities.keySet ()) {
                    try {
                        if (abstractUser instanceof AbstractUser) {
                            ((AbstractUser) abstractUser)
                                    .unlockLocation ();
                        }
                    } catch (final IllegalMonitorStateException e) {
                        // just eat it
                    }
                }
            }
        }

		final Coord2D com = ball.getCenterOfMass ();
		if (com.getX () < 0 || com.getX () > Room.MAX_X
				|| com.getY () < 0 || com.getY () > Room.MAX_Y
				|| Double.isNaN (com.getX ())
				|| Double.isNaN (com.getY ())) {
			AppiusClaudiusCaecus.reportBug ("com err "
					+ com.toString ());
		}

		// Stop the ball if it goes out of bounds
		if ( !field.contains (ball.getCenterOfMass ()) && !outOfBounds) {
			outOfBounds = true;
			inGoal = goalLeft.contains (ball.getCenterOfMass ())
					|| goalRight.contains (ball.getCenterOfMass ());
			if (inGoal) {
				speakRefSpeak (RefPhrases.goal);
				soccerField.goTo (referee, referee.getLocation (), "S",
						"Jumping");
				entryPoint = centerField;
				ball.setTravelRate (0d);
				if (goalLeft.contains (ball.getCenterOfMass ())) {
					rightScore++ ;
				} else {
					leftScore++ ;
				}
				lastGoalTime = currentTime;
			} else {
				speakRefSpeak (RefPhrases.outOfBounds);
				soccerField.goTo (referee, referee.getLocation (), "S",
						"Dance1");
				final Coord2D [] exitPoints = field
						.intersection (new LineSeg2D (ball
								.getCenterOfMass (), centerField));
				if (null != exitPoints && exitPoints.length > 0) {
					final Vector2D toCenter = new Vector2D (centerField
							.getX ()
							- exitPoints [0].getX (), centerField
							.getY ()
							- exitPoints [0].getY ()).normalize ()
							.scale (20d);
					entryPoint = new Coord2D (exitPoints [0].getX ()
							+ toCenter.getX (), exitPoints [0].getY ()
							+ toCenter.getY ());
				} else {
					entryPoint = centerField;
				}
			}
		}

		// If not moving, reset the ball back to the center field after
		// 5 minutes. Also move the ball back in bounds if it went out
		if (ball.getTravelRate () > 0.0 && !outOfBounds) {
			lastTimeMoving = currentTime;
		}

		if (outOfBounds && currentTime > lastTimeMoving + 2000) {
			if (null != entryPoint && field.contains (entryPoint)) {
				soccerField.putHere (ball, entryPoint.toCoord3D ());
			} else {
				soccerField.putHere (ball, centerField.toCoord3D ());
			}
			if (field.contains (ball.getCenterOfMass ())) {
				outOfBounds = false;
				inGoal = false;
				speakRefSpeak (RefPhrases.gameOn);

			}
		} else if ( (centerField.distance (ball.getLocation ()
				.toCoord2D ()) > 0d
				|| leftScore > 0 || rightScore > 0)
                && currentTime > lastTimeMoving + 300000) {
			soccerField.putHere (ball, centerField.toCoord3D ());
			speakRefSpeak (RefPhrases.newGame);
			lastGoalTime = currentTime;
			leftScore = 0;
			rightScore = 0;
		}

		if (currentTime > lastGoalTime + 5000) {
			speakRefSpeak (RefPhrases.score);
			lastGoalTime = currentTime + 30000;
		}

	}

}
