/*
 * Created on Jan 24, 2005
 */
package javax.realtime.test.airplane;

import javax.realtime.AbsoluteTime;
import javax.realtime.AsyncEvent;
import javax.realtime.RelativeTime;

import javax.realtime.DSS.DSS;
import javax.realtime.DSS.Normal;

/**
 * Class modeling the airplane -- a central part of the embedding code for this example
 * @author gary
 */
public class Airplane {
	
	// milliseconds
	private final double gearDeployMean = 28000.0;
	private final double gearDeployStandardDeviation = 5500.0;
	
	// flap change speed; one degree per second == 1000
	private final int millisecondsPerDegree = 1000;
	
	/**
	 * Constructor
	 * @param altitude current altitude of the airplane
	 */
	public Airplane( int altitude ) {
		assert altitude >= 0;
		this.altitude = altitude;
		gearChangeDelay = new Normal(gearDeployMean, gearDeployStandardDeviation);
	}
	
	/**
	 * Set the airplane's current altitude plan, i.e., to ascend or descend
	 * @param plannedAltitude the target altitude
	 * @param plannedAltitudeTime the time at which the target altitude will be reached
	 */
	public void setAltitudePlan( int plannedAltitude, AbsoluteTime plannedAltitudeTime ) {
		assert DSS.currentTime().compareTo( plannedAltitudeTime ) < 0;
		altitude = readAltitude();
		altitudeTime = DSS.currentTime();
		this.plannedAltitude = plannedAltitude;
		this.plannedAltitudeTime = plannedAltitudeTime;
	}
	
	/**
	 * Set the airplane's current altitude, at level flight.
	 * @param altitude the current altitude
	 */
	public void setAltitude( int altitude ) {
		this.altitude = altitude;
		// no climb or descent plan; level flight
		this.plannedAltitudeTime = null;
	}
	
	/**
	 * Read the airplane's current altitude.  Note that if there is an altitude plan
	 * in effect, this will require extrapolation of last altitude known explicitly.
	 * @return the current altitude
	 */
	public int readAltitude() {
		if ( plannedAltitudeTime == null ) {
			// level flight
			return altitude;
		}
		
		if ( DSS.currentTime().compareTo( plannedAltitudeTime ) >= 0 ) {
			// leveled off after reaching new altitude
			// remove flight plan, as optimization
			altitude = plannedAltitude;
			plannedAltitudeTime = null;
			return altitude;
		}
		
		// mid-plan; do linear extrapolation
		int altitudeDiff = plannedAltitude - altitude;
		RelativeTime timeDiff = plannedAltitudeTime.subtract( altitudeTime );
		RelativeTime nowDiff = DSS.currentTime().subtract( altitudeTime );
		
		// TO DO make calculation accurate to nanoseconds
		long millisTimeDiff = timeDiff.getMilliseconds();
		long millisNowDiff = nowDiff.getMilliseconds();
		float fraction = (float)millisNowDiff / (float)millisTimeDiff;
		return (int)(((float)altitudeDiff)*fraction) + altitude;
	}
	
	/**
	 * Return the current flaps setting in degrees
	 * @return the current flaps setting in degrees
	 */
	public int getFlapsDegrees() {
		assert flapsDegrees >= 0;
		return flapsDegrees;
	}
	
	/**
	 * Change the current flaps setting.  Note that this will require a delay of the
	 * invoking <A HREF="RealtimeThread.html"><CODE>RealtimeThread</CODE></A>, because
	 * actuation is not instantaneous.
	 * @param newDegrees the desired new flaps setting
	 * @param lightIndicator an event to fire when the flaps attain the desired setting; if
	 * <CODE>null</CODE>, simply return after the delay
	 */
	public void changeFlaps( int newDegrees, AsyncEvent lightIndicator ) 
		throws InterruptedException {
		// calculate delay, based on degree difference between desired and current
		// settings
		int degreeDifference = Math.abs( newDegrees - getFlapsDegrees() );
		int holdTimeMillis = degreeDifference * millisecondsPerDegree;
		
		printTimeDistanceAndAltitude();
		System.out.println( "request to change flaps from " + flapsDegrees + " to " +
				newDegrees );
		DSS.hold( new RelativeTime( holdTimeMillis, 0 ));
		
		// flaps setting now completed
		printTimeDistanceAndAltitude();
		System.out.println( "flaps changed from " + flapsDegrees + " to " +
				newDegrees );
		// update local variable recording flaps setting
		setFlaps( newDegrees );
		
		// fire event provided, if not <CODE>null</CODE>
		if ( lightIndicator != null ) {
			lightIndicator.fire();			
		}		
	}
	
	/**
	 * Invert state of landing gear, i.e., from up to down, or vice-versa.
	 * Action includes retraction of gear doors.<p>
	 * 
	 * Note this will require a delay, which figures significantly in the accident
	 * scenario.
	 * @param gearIndicator an event to fire when gear change and door retraction
	 * are completed; if
	 * <CODE>null</CODE>, simply return after the delay.
	 */
	public void changeGear( AsyncEvent gearIndicator ) 
		throws InterruptedException {
		printTimeDistanceAndAltitude();
		System.out.println( "gear change requested from " + 
				gearString( false ) + " to " + gearString( true ) );
		int delayMillis;
		if ( DSS.usingJPF ) {
			delayMillis = (int)gearChangeDelay.threeChoice();
		} else {
			delayMillis = (int)gearChangeDelay.draw();				
		}
		DSS.hold( new RelativeTime( delayMillis, 0 ) );
		
		printTimeDistanceAndAltitude();
		System.out.println( "gear change from " + gearString( false ) + " to " +
				gearString( true ) + " completed" );
		setGearDown( !getGearDown() );
		if ( gearIndicator != null ) {
			gearIndicator.fire();
		}
	}
	
	/**
	 * Set flaps degrees directly, without simulating delay.  Can be used for initialization.
	 * @param degrees the current flaps degrees
	 */
	public void setFlaps( int degrees ) {
		flapsDegrees = degrees;
	}
	
	/**
	 * Return the state of the landing gear; <CODE>true</CODE> iff gear is down.
	 * @return true iff the landing gear is down
	 */
	public boolean getGearDown() {
		return gearDown;
	}
	
	/**
	 * Set the state of the landing gear, without simulating delay.  Can be used for
	 * initialization.
	 * @param gearDown
	 */
	public void setGearDown( boolean gearDown ) {
		this.gearDown = gearDown;
	}
	
	/**
	 * Create a string conveying the status of the landing gear.
	 * @param invert if <CODE>true</CODE>, invert the state for reporting.  This is
	 * handy for printing state requested before it is attained.
	 * @return <CODE>up</CODE> or <CODE>down</CODE>
	 */
	public String gearString( boolean invert ) {
		if ( invert ) {
			return getGearDown() ? "up" : "down";
		}
		return getGearDown() ? "down" : "up";
	}
	
	/**
	 * Prints prefix to log lines, including simulated time, distance to runway, and
	 * current altitude.
	 */
	public void printTimeDistanceAndAltitude() {
		DSS.printTime();
		System.out.print( "altitude " + readAltitude() );
		if ( feetToRunwayTime != null ) {
			System.out.print( ", runway " + readFeetToRunway() + " ft., " );
		} else {
			System.out.print( " " );
		}			
	}
	
	/**
	 * Sets the current feet to runway.  Used during initialization.  Also record the
	 * current time, so that the distance to the runway can be extrapolated using the
	 * current airplane speed after some time has passed.
	 * @param feet the current feet to the runway.
	 */
	public void setFeetToRunway( int feet ) {
		feetToRunway = feet;
		feetToRunwayTime = DSS.currentTime();
	}
	
	/**
	 * Make a dynamic calculation of the current feet to the runway.  The distance and
	 * time of the last speed change are not updated; this will not be done until the
	 * next speed change.
	 * @return the feet to the runway.
	 */
	public int readFeetToRunway() {
		long feetToRunwayTimeMillis = feetToRunwayTime.getMilliseconds();
		long timeNowMillis = DSS.currentTime().getMilliseconds();
		int feetCovered = (int)(timeNowMillis - feetToRunwayTimeMillis ) *
			groundFeetPerSecond / 1000;
		// System.out.println( "feetCovered=" + feetCovered );
		return feetToRunway - feetCovered;
	}
	
	/**
	 * Set the current ground speed of the airplane.  Because the speed is changing,
	 * the prior extrapolation must be closed out by updating the feet to the
	 * runway and recording explicitly the current distance to the runway.
	 * @param feetPerSecond the new speed in feet per second
	 */
	public void setSpeed( int feetPerSecond ) {
		printTimeDistanceAndAltitude();		
		System.out.println( "speed set at " + feetPerSecond*3600/5280 + " mph" );
		
		// ground speed change, update basis for extrapolation
		feetToRunway = readFeetToRunway();
		feetToRunwayTime = DSS.currentTime();					
		groundFeetPerSecond = feetPerSecond;
		lastSpeedChange = DSS.currentTime();
	}
	
	/**
	 * Calculate the time to cover a specified distance at the current speed.
	 * @param feet the specified distance in feet
	 * @return the time required, in milliseconds
	 */
	public long millisToCoverFeetAtCurrentSpeed( int feet ) {
		assert feet >= 0;
		return feet * 1000 / groundFeetPerSecond;
	}
	
	/**
	 * Feet covered at current speed for given time interval
	 * @param interval the time interval
	 * @return the feet covered during the interval
	 */
	public int feetCovered( RelativeTime interval ) {
		assert interval.compareTo( new RelativeTime( 0, 0 ) ) >= 0;
		// TO DO make accurate to nanoseconds
		long millis = interval.getMilliseconds();
		return (int)(groundFeetPerSecond * millis / 1000);
	}
	
	/**
	 * Current flaps degrees, initially 0
	 */
	private int flapsDegrees = 0;
	
	/**
	 * Current altitude
	 */	
	private int altitude;
	/**
	 * Time at which altitude plan was set, if any; initially <CODE>null</CODE>,
	 * which indicates level flight.
	 */
	private AbsoluteTime altitudeTime = null;
	/**
	 * Target altitude of current altitude plan.  Valid only if <CODE>altitudeTime</CODE>
	 * is not <CODE>null</CODE>.
	 */
	private int plannedAltitude;
	
	/**
	 * Time at which altitude should equal <CODE>plannedAltitude</CODE>.
	 * Valid only if <CODE>altitudeTime</CODE> is not <CODE>null</CODE>.
	 */
	private AbsoluteTime plannedAltitudeTime;
	
	/**
	 * Feet to runway as of time <CODE>feetToRunwayTime</CODE>.
	 */
	private int feetToRunway;
	
	/**
	 * Time at which feet to runway equaled <CODE>feetToRunway</CODE>.
	 */	
	private AbsoluteTime feetToRunwayTime;
	
	/**
	 * Current airplane speed, in feet per second.
	 */
	private int groundFeetPerSecond;
	
	/**
	 * Time at which speed was last set.  If speed has never been set, equals
	 * <CODE>null</CODE>.
	 */
	public AbsoluteTime lastSpeedChange;
	
	/**
	 * Landing gear status; <CODE>true</CODE> indicates gear up, 
	 * <CODE>false</CODE> indicates gear down.  Not updated until gear changes
	 * are complete.
	 */
	private boolean gearDown = false;
	
	/**
	 * Pseudo random normal number generator, used for landing gear change delays.
	 */
	protected Normal gearChangeDelay;
	
	/**
	 * True iff spoilers are armed.
	 */
	public boolean spoilersArmed;
	
	/**
	 * True iff the localizer alive event has happened.
	 */
	public boolean localizerAlive = false;
	
	/**
	 * True iff the airplane is on the ground.  Initially <CODE>false</CODE>.
	 */
	public boolean onGround = false;
}
