package gizmoball.game;

import gizmoball.NotImplementedException;
import gizmoball.property.Propertyified;
import java.util.HashSet;
import java.util.Set;
import physics.Vect;

/**
 * Abstract base class for representing gizmos. This class provides a
 * superclass for gizmos, and implements some of the fundamental
 * properties of gizmos.
 * 
 * <p>A gizmo has a name, which serves as an identifier for
 * identifying the gizmo in the property list and in other contexts
 * where a unique identifier is required. An implementing class may
 * (and should) provide a mechanism for generating a default
 * name. Ensuring uniqueness of the names is left to the client.
 * 
 * <p>The <code>tick</code> method is called each quantum to simulate
 * the gizmo. If the gizmo is mobile, this should handle motion.
 * 
 * <p>Collision detection and resolution methods are provided by the
 * gizmos. Each gizmo has a <code>canCollideWith</code> method that
 * returns a <code>Collidability</code> result indicating whether it
 * knows how to collide with a certain other gizmo class, or whether
 * it never collides with it. Each gizmo also has
 * <code>hasCollidedWith</code> and <code>collide</code> methods for
 * detecting and handling collisions.
 *
 * @see Collidability
 *
 * @specfield name : String // an identifier for the gizmo
 * @specfield position : Vect // the gizmo's position on the board
 * @specfield lastPosition : Vect // the gizmo's previous position
 * on the board, used in collision handling
 * @specfield gameBoard : GameBoard // the game board associated with
 * this gizmo
 * @derivedfield incomingTriggers : Set = 
 * gameBoard.incomingTriggersByGizmo(this) // triggers that trigger
 * this gizmo
 * @derivedfield outgoing triggers : Set =
 * gameBoard.outgoingTriggersByGizmo(this) // triggers that this
 * gizmo triggers
 *
 * @author <a href="mailto:drkp@mit.edu">Dan R. K. Ports</a>
 * @version $Id: AbstractGizmo.java,v 1.10 2004/04/25 09:30:52 dan Exp $
 */
public abstract class AbstractGizmo extends Propertyified {

    protected String name;
    protected Vect position;
    protected Vect lastPosition;
    protected GameBoard gameBoard;
    
    /**
     * Creates a new <code>AbstractGizmo</code> instance with the
     * specified name, position, and gameboard
     *
     * @param name the name of the new gizmo
     * @param position a Vect representing the gizmo's initial
     * position
     * @param gameBoard the GameBoard containing this gizmo
     * @modifies this
     * @effects constructs this
     */
    public AbstractGizmo(String name, Vect position, GameBoard gameBoard) {
        this.name = name;
        this.position = position;
        this.gameBoard = gameBoard;
    } // AbstractGizmo constructor

    /**
     * Creates a new <code>AbstractGizmo</code> instance with the
     * specified name and position, and no associated gameboard yet.
     *
     * @param name the name of the new gizmo
     * @param position a Vect representing the gizmo's initial
     * position
     * @modifies this
     * @effects constructs this, with gameBoard = null
     */
    public AbstractGizmo(String name, Vect position) {
        this(name, position, null);
    } // AbstractGizmo constructor

    
    /**
     * Gets the value of name
     *
     * @return the value of name
     */
    public String getName() {
        return this.name;
    }

    /**
     * Sets the value of name
     *
     * @param argName Value to assign to this.name
     */
    public void setName(String argName) {
        this.name = argName;

        firePropertyObservers("Name", name);
    }

    /**
     * Gets the value of position
     *
     * @return the value of position
     */
    public Vect getPosition() {
        return this.position;
    }

    /**
     * Sets the value of position. The value of lastPosition is also
     * updated to the previous value of position.
     *
     * @param argPosition Value to assign to this.position
     * @effects lastPosition = position; position = argPosition
     */
    public void setPosition(Vect argPosition) {
        this.lastPosition = this.position;
        this.position = argPosition;

        firePropertyObservers("Position", position);
    }

    /**
     * Gets the value of lastPosition
     *
     * @return the value of lastPosition
     */
    public Vect getLastPosition() {
        return this.lastPosition;
    }

    /**
     * Get the GameBoard value.
     * @return the GameBoard value.
     */
    public GameBoard getGameBoard() {
        return gameBoard;
    }

    /**
     * Set the GameBoard value.
     * @param newGameBoard The new GameBoard value.
     * @modifies this
     * @effects gameBoard = newGameBoard
     */
    public void setGameBoard(GameBoard newGameBoard) {
        this.gameBoard = newGameBoard;

        firePropertyObservers("GameBoard", gameBoard);
    }

    /**
     * Set up the properties of the abstract gizmo
     *
     * @modifies properties
     * @effects adds the Name and Position properties
     */
    protected void setUpProperties() {
        super.setUpProperties();
        addProperty("Name");
        addProperty("Position");
    }
    
    /**
     * Simulate the gizmo's behavior for <code>time</code>
     * seconds. Note that this is a <i>platonic tick</i> that is
     * continuous, not discrete; this allows for a more correct
     * simulation of the gizmo's motion. See the design document for
     * more details.
     *
     * @param time the length of time to simulate in seconds
     * @throws IllegalArgumentException if time is not positive
     * @modifies this
     * @effects this gizmo is simulated for time seconds
     * @return true if collisions need to be recomputed for the
     * gizmo, e.g. if the gizmo's velocity changed.
     */
    public abstract boolean tick(double time);

    /**
     * Determine whether this gizmo class will ever collide with
     * another gizmo type, and whether it knows how to. This is
     * intended to be a property of the gizmo classes, not their
     * instances. (It would be a static method if Java were not so
     * broken.)
     *
     * @param other the other gizmo to check collidability with
     * @throws IllegalArgumentException if other == null
     * @return a <code>Collidability</code> value indicating the
     * collidability relationship between this gizmo and other
     */

    public abstract Collidability canCollideWith(AbstractGizmo other);

    /**
     * Determine when this gizmo has collided with another
     * gizmo. Collision detection is performed in a manner specific to
     * each gizmo type, since the size and shape of the gizmo (its
     * extent around its position vector) may vary.
     *
     * @requires this.canCollideWith(other.getClass()) == MAY_COLLIDE
     * @param other true <code>AbstractGizmo</code> to check for a
     * collision with
     * @return the time in seconds until gizmos this and other will
     * collide, or Double.POSITIVE_INFINITY if they will never
     * collide
     */
    public abstract double timeUntilCollisionWith(AbstractGizmo other);
    
    /**
     * Simulates a collision with another gizmo. This is handled in a
     * manner specific to each gizmo type.
     *
     * @param other the <code>AbstractGizmo</code> to collide with
     * @throws IllegalArgumentException if other == null
     * @requires this.canCollideWith(other.getClass()) != MAY_COLLIDE
     * @modifies this, other
     * @effects if <code>this.hasCollidedWith(other)</code>, the
     * collision between this and other is simulated; otherwise,
     * no effect
     */
    public abstract void collide(AbstractGizmo other);

    /**
     * Execute the gizmo's trigger procedure. This method is called
     * whenever the gizmo is triggered, either by a keypress or
     * collision. The behavior of the gizmo when triggered is left
     * entirely to the implementing subclass, except that when a
     * gizmo is triggered, it should not directly trigger all the
     * gizmos triggered by it.
     *
     * @param trigger the triggered trigger
     * @requires trigger.target = this
     * @modifies this
     * @effects this handles the received trigger
     */
    public abstract void fireTrigger(AbstractTrigger trigger);

    /**
     * Add an incoming trigger to the incoming trigger list. This
     * method actually passes the request off to the associated
     * GameBoard. It thus also registers the trigger with the
     * GameBoard (and, if a CollisionTrigger, the source gizmo as
     * well). It is provided here in order to allow the trigger lists
     * to be modified through the properties framework.
     *
     * @see GameBoard#addTrigger
     *
     * @param trigger the <code>AbstractTrigger</code> value to add
     * @throws IllegalStateException if gameboard = null
     * @throws IllegalArgumentException if trigger = null or if
     * trigger is a CollisionTrigger and trigger.source is not in
     * gameboard
     * @throws UnsupportedOperationException if trigger is not of a
     * trigger type supported by gameBoard
     * @modifies this, gameBoard
     * @effects incomingTriggers = incomingTriggers U trigger
     */
    public void addIncomingTrigger(AbstractTrigger trigger)
    {
        if (gameBoard == null) {
            throw new IllegalStateException("gameBoard is null");
        } // end of if (gameBoard == null)

        if (trigger == null) {
            throw new IllegalArgumentException("trigger is null");
        } // end of if (trigger == null)
        
        gameBoard.addTrigger(trigger);
    }

    /**
     * Add an outgoing trigger to the outgoing trigger list. This
     * method actually passes the request off to the associated
     * GameBoard. It thus also registers the trigger with the
     * GameBoard and the target gizmo. It is provided here in order to
     * allow the trigger lists to be modified through the properties
     * framework.
     *
     * @see GameBoard#addTrigger
     *
     * @param trigger the <code>CollisionTrigger</code> value to add
     * @throws IllegalStateException if gameBoard = null
     * @throws IllegalArgumentException if trigger = null or if
     * trigger.target is not in gameBoard
     * @throws UnsupportedOperationException if trigger is not of a
     * trigger type supported by gameBoard
     * @modifies this, gameBoard
     * @effects outgoingTriggers = outgoingTriggers U trigger 
     */
    public void addOutgoingTrigger(CollisionTrigger trigger)
    {
        if (gameBoard == null) {
            throw new IllegalStateException("gameBoard is null");
        } // end of if (gameBoard == null)

        if (trigger == null) {
            throw new IllegalArgumentException("trigger is null");
        } // end of if (trigger == null)
        
        gameBoard.addTrigger(trigger);
    }

    /**
     * Remove an incoming trigger from the incoming trigger list. This
     * method actually passes the request off to the associated
     * GameBoard. It thus also removes the trigger with the GameBoard
     * (and, if a CollisionTrigger, the source gizmo as well). It is
     * provided here in order to allow the trigger lists to be
     * modified through the properties framework.
     *
     * @see GameBoard#removeTrigger
     *
     * @param trigger the <code>AbstractTrigger</code> value to add
     * @throws IllegalStateException if gameboard = null
     * @throws IllegalArgumentException if trigger = null or if
     * trigger is a CollisionTrigger and trigger.source is not in
     * gameboard
     * @throws UnsupportedOperationException if trigger is not of a
     * trigger type supported by gameBoard
     * @modifies this, gameBoard
     * @effects incomingTriggers = incomingTriggers \ triggers 
     */
    public void removeIncomingTrigger(AbstractTrigger trigger)
    {
        if (gameBoard == null) {
            throw new IllegalStateException("gameBoard is null");
        } // end of if (gameBoard == null)

        if (trigger == null) {
            throw new IllegalArgumentException("trigger is null");
        } // end of if (trigger == null)
        
        gameBoard.removeTrigger(trigger);
    }

    /**
     * Remove an outgoing trigger from the incoming trigger list. This
     * method actually passes the request off to the associated
     * GameBoard. It thus also removes the trigger with the GameBoard
     * and the target gizmo. It is
     * provided here in order to allow the trigger lists to be
     * modified through the properties framework.
     *
     * @see GameBoard#addTrigger
     *
     * @param trigger the <code>CollisionTrigger</code> value to add
     * @throws IllegalStateException if gameBoard = null
     * @throws IllegalArgumentException if trigger = null or if
     * trigger.target is not in gameBoard
     * @throws UnsupportedOperationException if trigger is not of a
     * trigger type supported by gameBoard
     * @modifies this, gameBoard
     * @effects outgoingTriggers = outgoingTriggers \ trigger 
     */
    public void removeOutgoingTrigger(CollisionTrigger trigger)
    {
        if (gameBoard == null) {
            throw new IllegalStateException("gameBoard is null");
        } // end of if (gameBoard == null)

        if (trigger == null) {
            throw new IllegalArgumentException("trigger is null");
        } // end of if (trigger == null)
        
        gameBoard.removeTrigger(trigger);
    }

    /**
     * Get the set of incoming triggers for this gizmo.
     *
     * @throws IllegalStateException if gameBoard is null
     * @return an immutable <code>Set</code> view of incomingTriggers
     */
    public Set/*<AbstractTrigger>*/ getIncomingTriggers()
    {
        if (gameBoard == null) {
            throw new IllegalStateException("gameBoard is null");
        } // end of if (gameBoard == null)

        return gameBoard.getIncomingTriggersForGizmo(this);
    }
    
    /**
     * Get the set of incoming triggers for this gizmo.
     *
     * @throws IllegalStateException if gameBoard is null
     * @return an immutable <code>Set</code> view of incomingTriggers
     */
    public Set/*<CollisionTrigger>*/ getOutgoingTriggers()
    {
        if (gameBoard == null) {
            throw new IllegalStateException("gameBoard is null");
        } // end of if (gameBoard == null)

        return gameBoard.getOutgoingTriggersForGizmo(this);
    }
    
} // AbstractGizmo
