import java.util.*;

/**
 * A horizontal lane of traffic.  "Traffic" in this context can mean vehicles,
 * logs, or other obstacles.  Lanes can be grass, roadway, or river.
 *
 * @author Jim Glenn
 * @version 0.2 1/8/2009 changed for x-coordinates measured in frog widths
 * @version 0.1 10/7/2008
 */

public class Lane
{
    /**
     * Constants for lane types.
     */

    public static final int ROAD = 0;
    public static final int GRASS = 1;
    public static final int RIVER = 2;

    /**
     * The default visible width of a lane, in frog widths.
     */

    public static final int SCREEN_WIDTH = 15;

    /**
     * The maximum velocity of a lane, in frog widths per second.
     */

    private static final double VELOCITY_MAX = 7.5;

    /**
     * The width of this lane, as a proportion of the screen width.
     */

    private double width;

    /**
     * The type of this lane.
     */

    private int type;

    /**
     * The position within this lane of the left edge of the screen.
     */

    private double position;

    /**
     * The velocity of this lane, in screen widths per second.  Positive
     * velocities indicate right-to-left motion; negative velocities indicate
     * left-to-right motion.
     */

    private double velocity;

    /**
     * The obstacles in this lane.
     */

    private List obstacles;

    /**
     * Creates an empty grass lane one screen wide.  The lane will
     * be stationary and will start at the left edge of the screen.
     */

    public Lane()
    {
	this(GRASS, SCREEN_WIDTH);
    }

    /**
     * Creates an empty grass lane of the given width.  The lane will
     * be stationary and will start at the left edge of the screen.
     *
     * @param w a <CODE>double</CODE> at least 1.0
     */

    public Lane(double w)
    {
	this(ROAD, w);
    }

    /**
     * Creates an empty lane of the given type.  The lane will be stationary
     * and will start at the left edge of the screen.
     *
     * @param t one of the type constants
     * @param w a <CODE>double</CODE> at least 1.0
     */

    public Lane(int t, double w)
    {
	if (w < SCREEN_WIDTH)
	    throw new IllegalArgumentException("Width must be at least "
					       + SCREEN_WIDTH + ": "
					       + w);

	if (t < ROAD || t > RIVER)
	    throw new IllegalArgumentException("Invalid lane type: " + t);

	width = w;
	type = t;
	velocity = 0.0;
	position = 0.0;
	obstacles = new ArrayList();
    }

    /**
     * Returns the type of this lane.
     *
     * @return the type of this lane
     */

    public int getType()
    {
	return type;
    }

    /**
     * Returns the velocity of this lane.
     *
     * return the velocity of this lane
     */

    public double getVelocity()
    {
	return velocity;
    }

    /**
     * Sets the velocity of this lane.  The velocity is givenin screen widths
     * per second, with positive values for leftward movement and negative
     * values for rightward movement.
     *
     * @param v the new speed of this lane
     * @throws IllegalArgumentException if the magnitude of v is greater than
     * the maximum allowable
     */

    public void setVelocity(double v)
    {
	if (Math.abs(v) > getMaximumVelocity())
	    {
		throw new IllegalArgumentException("invalid velocity: " + v);
	    }

	velocity = v;
    }

    /**
     * Returns the maximum velocity for any lane.
     *
     * @return the maximum velocity
     */

    public static double getMaximumVelocity()
    {
	return VELOCITY_MAX;
    }

    /**
     * Creates a river lane with evenly spaced logs.
     * The number of logs is determined by the argument.  The spaces
     * between the logs will be the same size as the logs.  The width
     * of the lane will be exactly one screen.
     *
     * @param n a positive integer
     * @return a lane with n logs
     */

    public static Lane makeLogLane(int n)
    {
	if (n <= 0)
	    throw new IllegalArgumentException("n must be positive: " + n);

	Lane l = new Lane(RIVER, SCREEN_WIDTH);

	double logWidth = (double)SCREEN_WIDTH / (2 * n);

	for (int i = 0; i < n; i++)
	    l.addObstacle(new Log(logWidth), logWidth * i * 2);

	return l;
    }

    /**
     * Adds the given obstacle to this lane at the given position.
     *
     * @param o an obstacle compatible with this lane
     * @param p a position within this lane
     *
     * @throws IllegalArgumentException if the obstacle is not appropriate
     * for this lane or will not fit at the given position
     */

    public void addObstacle(Obstacle o, double p)
    {
	if (type == GRASS && !(o instanceof Home))
	    throw new IllegalArgumentException("Only homes allowed on grass");
	else if (type == ROAD && !(o instanceof Vehicle))
	    throw new IllegalArgumentException("Only vehicles allowed on road");
	else if (type == RIVER && !(o instanceof Floater))
	    throw new IllegalArgumentException("Only floating obstacles allowed in river");

	if (p < 0.0 || p + o.getWidth() > width)
	    throw new IllegalArgumentException("Obstacle does not fit in lane:"
					       + "(" + p + ", " + (p + o.getWidth()) + ") "
					       + width);

	obstacles.add(new Pair(o, new Double(p)));
    }

    /**
     * Updates this lane and everything in it.
     *
     * @param t the time since the last update, in seconds
     */

    public void update(double t)
    {
	// update position

	position += t * velocity;

	if (position >= width)
	    position -= (int)(position / width) * width;
	else if (position < 0.0)
	    position += ((int)(-position / width) + 1) * width;

	// update obstacles in this lane

	Iterator i = obstacles.iterator();
	while (i.hasNext())
	    {
		Pair p = (Pair)(i.next());
		Obstacle o = (Obstacle)(p.getFirst());

		o.update(t);
	    }

	// update animals in this lane

	// check for appearance of new animals
    }

    /**
     * Finds the home overlapping the given position in this lane.
     *
     * @param x1 the left edge of the area to check for overlap with a home
     * @param x2 the right edge of the area to check for overlap with a home
     * @return the home overlapping that area, or <CODE>null</CODE>
     */

    public Home findHome(double x1, double x2)
    {
	// we take advantage of the fact that the home lane doesn't move
	// so we don't have to worry about wraparound

	Iterator i = obstacles.iterator();
	while (i.hasNext())
	    {
		Pair p = (Pair)(i.next());
		Obstacle o = (Obstacle)(p.getFirst());
		
		if (o instanceof Home)
		    {
			double homePos = ((Double)(p.getSecond())).doubleValue();
			if (x1 >= homePos && x2 <= homePos + o.getWidth())
			    return (Home)o;
		    }
	    }
	return null;
    }

    /**
     * Determines if this lane has a log at the given position.
     *
     * @param x the x position, relative to the edge of the screen
     * @return true if and only if this lane has a log at that position
     */

    public boolean hasFloaterAt(double x)
    {
	if (type != RIVER)
	    return false;

	x += position;
	if (x > width)
	    x -= width;

	Iterator i = obstacles.iterator();
	while (i.hasNext())
	    {
		Pair p = (Pair)(i.next());
		Obstacle o = (Obstacle)(p.getFirst());
		double obstaclePos = ((Double)(p.getSecond())).doubleValue();

		if (x >= obstaclePos
		    && x <= obstaclePos + o.getWidth()
		    && o instanceof Floater
		    && ((Floater)o).isAboveWater())
		    return true;
	    }
	return false;
    }

    /**
     * Determines if there is a collision in this lane between the
     * object between the given points and a lethal obstacle.
     * Positions are given relative to the edge of the screen.
     *
     * @param x1 the left edge of the area to check for collisions in
     * @param x2 the right edge of the area to check for collisions in
     */

    public boolean hasCollision(double x1, double x2)
    {
	return hasCollisionLaneCoordinates(x1 + position, x2 + position);
    }

    /**
     * Determines if there is a collision in this lane between the
     * object between the given points and a lethal obstacle.
     * Positions are given relative to the edge of this lane
     *
     * @param x1 the left edge of the area to check for collisions in
     * @param x2 the right edge of the area to check for collisions in
     */

    private boolean hasCollisionLaneCoordinates(double x1, double x2)
    {
	if (x1 > width)
	    {
		x1 -= width;
		x2 -= width;
	    }

	if (x2 > width)
	    {
		return (hasCollisionLaneCoordinates(x1, width)
			|| hasCollisionLaneCoordinates(0.0, x2 - width));
	    }
	else
	    {
		Iterator i = obstacles.iterator();
		while (i.hasNext())
		    {
			Pair p = (Pair)(i.next());
			Obstacle o = (Obstacle)(p.getFirst());
			double obsLeft = ((Double)(p.getSecond())).doubleValue();
			double obsRight = obsLeft + o.getWidth();
			
			if (o.isLethal()
			    && ((x1 < obsLeft && x2 > obsRight)
				|| (x1 > obsLeft && x1 < obsRight)
				|| (x2 > obsLeft && x2 < obsRight)))
			    {
				return true;
			    }
		    }
		return false;
	    }
    }
	

    /**
     * Returns an iterator over all the visible obstacles in this lane.
     * Obstacles will be visible if any portion of them is currently
     * positioned between 0.0 and the width of the screen.
     * Obstacles may be iterated over
     * twice if they are visible at both edges of the screen.
     * The iterator will return (obstacle, position) pairs, where the
     * position is relative to the edge of the screen.
     *
     * @return an iterator over this lane's obstacles
     */

    public Iterator obstacleIterator()
    {
	// create a list of visible obstacles

	List visible = new LinkedList();

	// get position within this lane of the left and right edges
	// of the screen -- we must do the wraparound carefully:
	// screenLeft + visibleWidth - width may come out >
	// screenLeft, in which case we think there's only a tiny
	// sliver visible and nothing gets painted

	double screenLeft = position;
	double screenRight = screenLeft + SCREEN_WIDTH;
	if (screenRight > width)
	    screenRight = screenLeft + (SCREEN_WIDTH - width);

	Iterator i = obstacles.iterator();
	while (i.hasNext())
	    {
		Pair p = (Pair)(i.next());
		Obstacle o = (Obstacle)(p.getFirst());
		double obsLeft = ((Double)(p.getSecond())).doubleValue();
		double obsRight = obsLeft + o.getWidth();

		if (screenLeft < screenRight)
		    {
			// no wraparound -- obstacle is visible exactly once
			// if either end is on screen

			if ((obsLeft > screenLeft && obsLeft < screenRight)
			    || (obsRight > screenLeft && obsRight < screenRight))
			    visible.add(new Pair(o, new Double(obsLeft - screenLeft)));
		    }
		else
		    {
			// wrapround -- obstacle is entirely visible
			// if its left is > screen left, visible on the
			// left edge if its right is > screen left
			// and (possibly also) on the right edge if
			// its left edge < screen right

			if (obsLeft > screenLeft)
			    visible.add(new Pair(o, new Double(obsLeft - screenLeft)));
			else
			    {
				if (obsRight > screenLeft)
				    visible.add(new Pair(o, new Double(obsLeft - screenLeft)));
				if (obsLeft < screenRight)
				    visible.add(new Pair(o, new Double(obsLeft + width - screenLeft)));
			    }
		    }
	    }

	// return an iterator over the list

	return visible.iterator();
    }
}

