/**
 * <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 java.util.concurrent.locks.ReentrantLock;

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.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.AbstractUser;
import org.starhope.appius.user.events.ActionHandler;
import org.starhope.appius.user.events.ActionMethod;
import org.starhope.appius.user.events.Quaestor;

import com.tootsville.npc.Volleyball;

/**
 * @author brpocock@star-hope.org
 * @author ewinkelman
 */
public class VolleyballCourt extends GameEvent {

	/**
     *
     */
    private static final long serialVersionUID = -3143375737691754345L;

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

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

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

    /**
     * The polygon that contains the hit space for the net
     */
    private final Polygon net;

    /**
     * Soccer field room object
     */
    private final Room volleyballCourt;

    /**
     * Center of left field
     */
    private final Coord2D leftCenter;

    /**
     * Center of right field
     */
    private final Coord2D rightCenter;

    /**
     * Last x coordinate of the last transmission
     */
    private int lastX = 0;

    /**
     * Last y coordinate of the last transmission
     */
    private int lastY = 0;

    /**
     * Stores the real location of the volleyball
     */
    private Coord3D realBallLocation;

    /**
     * Stores the real velocity of the volleyball TODO: Change to a
     * vector 3D class
     */
    private Coord3D realBallVelocity;

    /**
     * Lock object for updating/reading real coordinates
     */
	final ReentrantLock realLock = new ReentrantLock ();

    /**
     * @param z zone
     * @throws GameLogicException WRITEME
     * @throws NotFoundException WRITEME
     */
    public VolleyballCourt (final Zone z)
    throws GameLogicException, NotFoundException {
		super (z, 'Z');
        gameState = GameStateFlag.GAME_SOLO;
        volleyballCourt = z.getRoomByName ("bigTootoonaVolleyball");
        if (null == volleyballCourt) {
            AppiusClaudiusCaecus
                    .reportBug ("Can't find the volleyball court");
            throw new GameLogicException (
                    "Can't find the volleyball court", this, z);
        }
        rooms.add (volleyballCourt);
        volleyballCourt.add (this);

        Polygon temp = Geometry.stringToNewPolygon (volleyballCourt
                .getPlaceStringByName ("evt_$leftField"));
        leftField = null == temp || temp.getPoints ().length < 4 ? new Polygon (
                new Coord2D (100, 300), new Coord2D (380, 300),
                new Coord2D (380, 550), new Coord2D (100, 550))
                : temp;
        leftCenter = leftField.getBoundingCircle ().getCenter ();
        temp = Geometry.stringToNewPolygon (volleyballCourt
                .getPlaceStringByName ("evt_$rightField"));
        rightField = null == temp || temp.getPoints ().length < 4 ? new Polygon (
                new Coord2D (420, 300), new Coord2D (700, 300),
                new Coord2D (700, 550), new Coord2D (420, 550))
                : temp;
        rightCenter = rightField.getBoundingCircle ().getCenter ();
        temp = Geometry.stringToNewPolygon (volleyballCourt
                .getPlaceStringByName ("evt_$centerField"));
        net = null == temp || temp.getPoints ().length < 4 ? new Polygon (
                new Coord2D (380, 300), new Coord2D (420, 300),
                new Coord2D (420, 550), new Coord2D (300, 550))
                : temp;
        ball = new Volleyball ();
        ball.setTravelRate (0d);
        volleyballCourt.join (ball);
        dropBall (rightCenter, 100);

        PhysicsScheduler.start (this);

        Quaestor.listen (new ActionHandler (volleyballCourt, null,
                "op.put", ball, 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 (null != trailer && trailer.length > 0
                                && trailer [0] instanceof Coord3D) {
							realLock.lock ();
							try {
                                final Coord3D newCoord3d = (Coord3D) trailer [0];
                                dropBall (newCoord3d.toCoord2D (), 100);
                            } finally {
									realLock.unlock ();
							}
                        }
						return false;
                    }
                }));
    }

    /**
     * @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
	}

    /**
	 * Gets where the clients think the ball is, taking into account the
	 * Z offset (2D projection of 3D coördinates)
	 *
	 * @return where the clients think the ball is, taking into account
	 *         the Z offset
	 */
    private Coord2D ballLocation () {
        return new Coord2D (realBallLocation.getX (), realBallLocation
                .getY ()
                - realBallLocation.getZ ());
    }

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

    /**
     * Drops the ball from the given height at the specified coordinates
     *
     * @param coord2d Drop location
     * @param height Drop height
     */
	void dropBall (final Coord2D coord2d, final double height) {
        realBallLocation = new Coord3D (coord2d.getX (), coord2d
                .getY (), height);
        realBallVelocity = new Coord3D (0, 0, 0);
        updateBallLocation ();
    }

    /**
     * @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 ();
    }

	/**
     * @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 ()) : "Volleyball travel rate is NaN";
        if (deltaTime > 1000) {
			return;
		}


		realLock.lock ();
		try {

            double newZ = Math.max (realBallLocation.getZ ()
                    + realBallVelocity.getZ () * deltaTime / 1000d, 0d);
            double newX = realBallLocation.getX ()
                    + realBallVelocity.getX () * deltaTime / 1000d;
            double newY = realBallLocation.getY ()
                    + realBallVelocity.getY () * deltaTime / 1000d;
            // Calculate bounces, but don't let it bounce forever
            if (newZ == 0) {
                Coord3D newBallVelocity = realBallVelocity
                        .multiply (0.4);
                newBallVelocity = new Coord3D (newBallVelocity.getX (),
                        newBallVelocity.getY (), -newBallVelocity
                                .getZ ());
                if (newBallVelocity.getZ () < 1d) {
                    newBallVelocity = new Coord3D (0, 0, 0);
                }
                realBallVelocity = newBallVelocity;
            } else {
                realBallVelocity = realBallVelocity.add (0, 0, -0.1d
                        * deltaTime);
            }
            realBallLocation = new Coord3D (newX, newY, newZ);

            // 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);
                ballCollider.setCenterOfMass (ballLocation ());
                ballCollider.reset ();
                for (final AbstractUser abstractUser : volleyballCourt
                        .getAllUsers ()) {
                    if (abstractUser == ball) {
                        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,
                                        volleyballCourt.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 ()) {
                    realBallLocation = new Coord3D (ballCollider
                            .getCenterOfMass ().getX (), ballCollider
                            .getCenterOfMass ().getY (),
                            realBallLocation.getZ ());
                    updateBallLocation ();
                    Vector2D newVel = ballCollider.getVelocity ();
                    if (newVel.length () != 0) {
						newVel = newVel.normalize ();
					}
                    newVel = newVel.scale (180);
                    realBallVelocity = new Coord3D (newVel.getX (),
                            newVel.getY (), 100);
                }
            } 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 ());
            }

            updateBallLocation ();
        } finally {
			realLock.unlock ();

        }
    }

	/**
	 * Updates the ball's location
	 */
    private void updateBallLocation () {
        final Coord2D temp = ballLocation ();
        final int newX = (int) temp.getX ();
        final int newY = (int) temp.getY ();
        if (lastX != newX || lastY != newY) {
            lastX = newX;
            lastY = newY;
            try {
                ball.getLocationForUpdate ();
                volleyballCourt.putHere (ball, temp.toCoord3D ());
                volleyballCourt.notifyUserAction (ball);
            } finally {
                try {
                    ball.unlockLocation ();
                } catch (Exception e) {
                    // Eat it
                }
            }
        }
    }

}
