package gizmoball.game;

import junit.framework.Assert;

import physics.*;

/**
 * An AbstractGizmo with added logic for dealing with bumper-like
 * collisions of gizmos with polygonal geometry.
 *
 * <p>Subclasses must call the {@link #setGeometry(Vect[])} to set up
 * the polygon boundary that will be used for collision detection and
 * resolution.
 *
 * <p>Collision resolution handles bumper-like collisions with ball
 * gizmos with a coefficient of reflection of 1.0.  Note that
 * collisions are only between the ball and the boundary of the
 * gizmo.  It has no concept of solidness.
 *
 * @specfield geometry : List<Vect> // The points of this gizmo's
 *                                     polygon
 * @specfield orientation : Angle   // The rotation of this gizmo
 *
 * @author <a href="mailto:amdragon@mit.edu">Austin Clements</a>
 * @version 1.0
 */
public abstract class AbstractGizmoWithPolygonalGeometry
    extends AbstractGizmo {
    /* Abstraction function
     *   geometry = rotatedPoints
     *   orientation = orientation
     */
    
    private LineSegment[] edges = new LineSegment[0];
    private Circle[] corners = new Circle[0];
    private Vect[] realPoints = new Vect[0];
    private Vect[] transformedPoints = new Vect[0];
    private Vect center = Vect.ZERO;

    private Angle orientation = Angle.ZERO;

    /**
     * Check representation invariant
     */
    protected void checkRep() {
        if (BuildOptions.FAST_CHECK_REP) {
            return;
        }

        // Test nullnesses
        Assert.assertNotNull(edges);
        Assert.assertNotNull(corners);
        Assert.assertNotNull(realPoints);
        Assert.assertNotNull(transformedPoints);
        Assert.assertNotNull(center);
        Assert.assertNotNull(orientation);

        // Confirm all arrays are the same length
        Assert.assertTrue(realPoints.length == edges.length);
        Assert.assertTrue(realPoints.length == corners.length);
        Assert.assertTrue(realPoints.length == transformedPoints.length);

        // Confirm that corners match up with the rotated points
        for (int i = 0; i < transformedPoints.length; ++i) {
            Assert.assertEquals(transformedPoints[i], corners[i].getCenter());
        }

        // Confirm that edges match up with rotated points
        for (int i = 0; i < edges.length; ++i) {
            Assert.assertEquals(transformedPoints[i], edges[i].p1());
            Assert.assertEquals(transformedPoints[(i+1)%
                                                  transformedPoints.length],
                                edges[i].p2());
        }
    }

    /**
     * Creates a new <code>AbstractGizmoWithPolygonalGeometry</code>
     * instance.
     *
     * @param name the name of the new gizmo
     * @param position a Vect representing the gizmo's initial
     * position
     * @modifies this
     * @effects constructs this
     */
    public AbstractGizmoWithPolygonalGeometry(String name, Vect position) {
        super(name, position);

        checkRep();
    }

    /*
     * Geometry
     */
    
    /**
     * Set the geometry for this gizmo.  This is used by the default
     * implementations of {@link
     * #timeUntilCollisionWith(AbstractGizmo)} and {@link
     * #collide(AbstractGizmo)} in this class.  This should be the
     * geometry with orientation 0.  Rotation of the geometry will be
     * performed about center.
     *
     * @param points the array of points describing the vertexes of
     * the polygon shape of this gizmo
     * @param center the center of rotation for this gizmo
     * @modifies geometry
     * @effects sets the points of the gizmo geometry
     */
    protected void setGeometry(Vect[] points, Vect center) {
        edges = new LineSegment[points.length];
        corners = new Circle[points.length];
        realPoints = new Vect[points.length];
        transformedPoints = new Vect[points.length];
        this.center = center;

        for (int i = 0, len = points.length; i < len; ++i) {
            realPoints[i] = new Vect(points[i].x(), points[i].y());
        }

        transformPoints();

        checkRep();
    }
    
    /**
     * Set the geometry for this gizmo.  This is used by the default
     * implementations of {@link
     * #timeUntilCollisionWith(AbstractGizmo)} and {@link
     * #collide(AbstractGizmo)} in this class.  This should be the
     * geometry with orientation 0.  Rotation of the geometry will be
     * performed about center. The center is assumed by default to lie
     * at the origin.
     *
     * @param points the array of points describing the vertexes of
     * the polygon shape of this gizmo
     * @modifies geometry
     * @effects sets the points of the gizmo geometry, with center at
     * the origin
     */
    protected void setGeometry(Vect[] points) {
        setGeometry(points, new Vect(0,0));
    }
    
    /**
     * Rotate the points of the geometry by orientation and update the
     * edges and corners.
     *
     * @requires edges, points, and transformedPoints are allocated to the
     * right dimension
     * @modifies transformedPoints, edges, corners
     * @effects rotates the geometry of this gizmo
     */
    private void transformPoints() {
        Vect position = getPosition();
        
        // Rotate the points
        for (int i = 0, len = realPoints.length; i < len; ++i) {
            transformedPoints[i] =
                realPoints[i].minus(center).rotateBy(orientation).
                plus(center).plus(position);
        }

        // Find new edges and corners
        for (int i = 0, len = transformedPoints.length; i < len; ++i) {
            edges[i] = new LineSegment(transformedPoints[i],
                                       transformedPoints[(i+1)%len]);
            corners[i] = new Circle(transformedPoints[i], 0);
        }
    }

    /**
     * Get the geometry for this gizmo.  This is the same format as
     * used in {@link #setGeometry(Vect[])}.  The returned vectors
     * reflect the current position and rotation (they are absolute
     * with respect to the board).  Do not modify the returned array.
     *
     * @return the array of points describing the vertexes of the
     * shape of this gizmo on the board
     */
    public Vect[] getGeometry() {
        checkRep();
        
        return transformedPoints;
    }


    /*
     * Properties
     */

    /**
     * Get the orientation of this gizmo
     *
     * @return the orientation of this gizmo
     */
    public Angle getOrientation() {
        checkRep();
        
        return orientation;
    }

    /**
     * Set the orientation of this gizmo.  This also updates the gizmo
     * geometry to reflect the new orientation.
     *
     * @param newOrientation the new orientation of this gizmo
     */
    public void setOrientation(Angle newOrientation) {
        checkRep();
        
        orientation = newOrientation;

        transformPoints();
        firePropertyObservers("Orientation", newOrientation);

        checkRep();
    }

    public void setPosition(Vect newPosition) {
        checkRep();
        
        super.setPosition(newPosition);
        // FIXME The transform should occur before observers are fired
        transformPoints();
    }

    /**
     * Set up the properties of an AbstractGizmoWithPolygonalGeometry
     *
     * @modifies properties
     * @effects adds the Orientation property
     */
    protected void setUpProperties() {
        super.setUpProperties();
        addProperty("Orientation");
    }


    /*
     * Collision system
     */

    /**
     * AbstractGizmoWithPolygonalGeometry's know how to collide with
     * balls and with other AbstractGizmoWithPolygonalGeometry's.
     *
     * @param other the gizmo to test collidability with
     * @return NEVER_COLLIDE for AbstractGizmoWithPolygonalGeometry's,
     * CAN_COLLIDE for balls, and DEFER_COLLIDE otherwise
     */
    public Collidability canCollideWith(AbstractGizmo other) {
        checkRep();
        
        if (other instanceof AbstractGizmoWithPolygonalGeometry) {
            return Collidability.NEVER_COLLIDE;
        } else if (other instanceof BallGizmo) {
            return Collidability.CAN_COLLIDE;
        } else {
            return Collidability.DEFER_COLLIDE;
        }
    }

    /**
     * Computed collision information.  These values are updated by
     * {@link #computeBallCollision(BallGizmo).
     */
    private double bestEdgeTime = Double.POSITIVE_INFINITY;
    private LineSegment bestEdge = null;
    private double bestPointTime = Double.POSITIVE_INFINITY;
    private Circle bestPoint = null;

    /**
     * Compute which edge or point a ball will collide with, and at
     * what times.  If the ball will not collide with any edge/point,
     * their corresponding times will be Double.POSITIVE_INFINITY.
     * Because this produces four values, the results are kept in the
     * bestEdgeTime, bestEdge, bestPointTime, and bestPoint fields.
     *
     * @param ball the ball to compute collision with
     * @modifies bestEdgeTime, bestEdge, bestPointTime, bestPoint
     * @effects bestEdgeTime and bestEdge get the earliest edge
     * collision time and edge, respectively.  bestPointTime and
     * bestPoint get the earliest point collision time and point,
     * respectively.
     */
    private void computeBallCollision(BallGizmo ball) {
        Circle ballCircle = ball.getCircle();
        Vect ballVelocity = ball.getVelocity();

        // Check ball-wall collisions
        bestEdgeTime = Double.POSITIVE_INFINITY;
        bestEdge = null;
        for (int i = 0, len = edges.length; i < len; ++i) {
            double thisTime =
                Geometry.timeUntilWallCollision(edges[i],
                                                ballCircle, ballVelocity);
            if (thisTime < bestEdgeTime) {
                bestEdgeTime = thisTime;
                bestEdge = edges[i];
            }
        }

        // Check ball-point collisions
        bestPointTime = Double.POSITIVE_INFINITY;
        bestPoint = null;
        for (int i = 0, len = corners.length; i < len; ++i) {
            double thisTime =
                Geometry.timeUntilCircleCollision(corners[i],
                                                  ballCircle, ballVelocity);
            if (thisTime < bestPointTime) {
                bestPointTime = thisTime;
                bestPoint = corners[i];
            }
        }
    }

    /**
     * Determines the time until a collision with a ball.  Because
     * AbstractGizmoWithPolygonalGeometry's are fixed, they cannot
     * collide with each other.
     *
     * @requires other instanceof BallGizmo
     * @param other the gizmo to test collision time with
     * @return the time until collision with other, or
     * Double.POSITIVE_INFINITY if no collision will occur
     */
    public double timeUntilCollisionWith(AbstractGizmo other) {
        checkRep();
        
        BallGizmo ball = (BallGizmo)other;

        computeBallCollision(ball);

        if (bestPointTime <= bestEdgeTime) {
            return bestPointTime;
        } else {
            return bestEdgeTime;
        }
    }

    /**
     * Resolve a collision with a ball.  This reflects the ball off
     * whatever face (or point) it collided with.  This assumes the
     * ball is approximately one radius away from one of the faces.
     *
     * @requires other instanceof BallGizmo
     * @param other the gizmo to collide with
     */
    public void collide(AbstractGizmo other) {
        checkRep();

        BallGizmo ball = (BallGizmo)other;
        Circle ballCircle = ball.getCircle();
        Vect ballVelocity = ball.getVelocity();

        computeBallCollision(ball);

        if (bestPointTime <= bestEdgeTime) {
            // Collision with point
            Vect newVelocity = Geometry.reflectCircle(bestPoint.getCenter(),
                                                      ballCircle.getCenter(),
                                                      ballVelocity);
            ball.setVelocity(newVelocity);
        } else {
            // Collision with edge
            Vect newVelocity = Geometry.reflectWall(bestEdge, ballVelocity);
            ball.setVelocity(newVelocity);
        }

        checkRep();
    }
}
