import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.IOException;
import java.util.Random;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Container;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.FontMetrics;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JComponent;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JTextArea;
import javax.swing.BorderFactory;
import javax.swing.border.BevelBorder;

/**
 * Display a graphical quiz on holding entries.  This is a JApplet, so it can
 * be put into browsers, or can run standalone from a jre.  (This code is
 * Copyright Ted Faber October 2002)
 * @author Ted Faber <a href="mailto:faber@lunabase.org">faber@lunabase.org</a>
 */
public class HoldQuiz extends JApplet {
    /** aircraft heading to the fix */
    protected int heading;
    /** radial form the fix on which to hold */
    protected int radial;
    /** Left turning hold?? */
    protected boolean leftTurns;
    /** Altitude */
    int alt;
    /** EFC time */
    int efc;
    /** Source of randomness for this quiz. */
    Random rand;
    /** The heading/diagram display */
    HQPanel hqPanel;
    /** Buttons */
    JButton b1, b2;
    /** The area for displaying messages */
    JTextArea msg;
    /** Toggle whether the airplane is always pointed up or changes heading in
     * the display. */
    JCheckBox up_p;
    /** When this quiz started */
    long started;

    /* ActionListeners */
    /** Listener active when the timer is stopped and solution is displayed */
    IdleQuiz idleQuiz;
    /** Listener active when an question is active */
    PendingQuestion pendingQuestion;
    /** Controls the HQPanel display */
    SwitchView panel;

    static protected enum DisplayType { HI, DIAGRAM, BOTH };

    protected class HQPanel extends JPanel {
	/** Which visualization to display. */
	protected DisplayType display;
	/** True if the aircraft changes heading, false if it points up,
	 * independent of heading when the diagram is drawn alone. */
	protected boolean northIsUp;

	/**
	 * Simple constructor.  Fill in a preferred size and a border.
	 */
	HQPanel() { 
	    super(); 
	    display = DisplayType.HI;
	    northIsUp = true;
	    setPreferredSize(new Dimension(250,250)); 
	    setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
	}

	/**
	 * Redraw the component by using a combination of the paint functions.
	 */
	public void paintComponent(Graphics g) {
	    switch (display) {
		case HI:
		    paintHI(g);
		    break;
		case DIAGRAM:
		    paintDiagram(g, false);
		    break;
		case BOTH:
		    paintHI(g);
		    paintDiagram(g, true);
		    break;
	    }
	}

	/**
	 * Draw the HI display.  Basically straight lines are drawn and
	 * labelled, but the transform is slowly rotated around to create a
	 * dial.
	 * @param g the current graphics context
	 */
	protected void paintHI(Graphics g) {
	    Graphics2D g2 = (Graphics2D) g;
	    FontMetrics fm = g2.getFontMetrics(g.getFont());
	    AffineTransform restoreXform = g2.getTransform();
	    AffineTransform marksXform = (AffineTransform) restoreXform.clone();
	    AffineTransform dialXform = (AffineTransform) restoreXform.clone();
	    // Constants for drawing the HI, basically identified as constants
	    // so that the compiler will use them as such.
	    final Dimension d = getSize();
	    final int faceCenterX = d.width/2;
	    final int faceCenterY = d.height/2;
	    final int faceRad = (Math.min(d.height, d.width)* 8) /20;
	    final int longLine = faceRad/10;
	    final int shortLine = faceRad/20;
	    final double incr5 = degreesToRadians(5);
	    final double incr45 = degreesToRadians(45);

	    g2.setColor(Color.black);
	    g2.fillRect(0, 0, d.width, d.height);
	    g2.fillOval(faceCenterX-faceRad, faceCenterY-faceRad, 
		2*faceRad, 2*faceRad);
	    g2.setColor(Color.white);
	    g2.drawOval(faceCenterX-faceRad, faceCenterY-faceRad, 
		2*faceRad, 2*faceRad);

	    // Pointer triangle
	    g2.drawLine(faceCenterX, faceCenterY-faceRad, 
		    faceCenterX - longLine, faceCenterY-faceRad - longLine);
	    g2.drawLine(faceCenterX - longLine , faceCenterY-faceRad -longLine, 
		    faceCenterX + longLine, faceCenterY-faceRad - longLine);
	    g2.drawLine(faceCenterX, faceCenterY-faceRad, 
		    faceCenterX + longLine, faceCenterY-faceRad - longLine);

	    // 90 and 45 degree marks
	    for ( int i = 1; i < 8 ; i++ ) {
		final int len = ( i % 2 == 0 ) ? longLine : shortLine;
		marksXform.rotate(incr45, faceCenterX, faceCenterY);
		g2.setTransform(marksXform);
		g2.drawLine(faceCenterX, faceCenterY-faceRad-2,
		    faceCenterX, faceCenterY-faceRad -  len - 2);
	    }

	    // Global spin to put the face in the right place
	    dialXform.rotate(-degreesToRadians(heading), 
		faceCenterX, faceCenterY);
	    g2.setTransform(dialXform);

	    // Walk around by 5 degrees a step, putting in lines and labels
	    for (int i = 0; i < 72; i++ ) {
		g2.drawOval(faceCenterX-1, faceCenterY-1, 2, 2);
		if ( i % 2 == 0 ) 
		    g2.drawLine(faceCenterX, faceCenterY-faceRad,
			faceCenterX, faceCenterY-faceRad + longLine);
		else 
		    g2.drawLine(faceCenterX, faceCenterY-faceRad,
			faceCenterX, faceCenterY-faceRad + shortLine);

		if ( i % 6  == 0) {
		    String s = Integer.toString(i*5);
		    Rectangle2D bounds = fm.getStringBounds(s, g);
		    int centering = (int) (bounds.getWidth()/2);

		    g2.drawString(s, faceCenterX-centering, 
			faceCenterY-faceRad+2 * longLine);
		}
		dialXform.rotate(incr5, faceCenterX, faceCenterY);
		g2.setTransform(dialXform);
	    }
	    g2.setTransform(restoreXform);
	}
	/** Accessors for the "north is up property" */
	public boolean setNorthIsUp(boolean v) { 
	    northIsUp = v; 
	    repaint();
	    return northIsUp;
	}
	public boolean getNorthIsUp() { return northIsUp;}

	/**
	 * Draw the hold display.  Again, liberal use of transforms gets the
	 * most out of a few simple drawing operations.
	 * @param g the graphics context
	 * @param overlay true if this is an overlay drawing: don't clear and
	 * no AC.  Ignore northIsUp
	 */
	protected void paintDiagram(Graphics g, boolean overlay) {
	    Graphics2D g2 = (Graphics2D) g;
	    AffineTransform restoreXform = g2.getTransform();
	    AffineTransform planeXform = (AffineTransform) restoreXform.clone();
	    AffineTransform holdXform = (AffineTransform) planeXform.clone();
	    // Constants
	    final Dimension d = getSize();
	    final Insets insets = getInsets();
	    final int centerX = d.width/2;
	    final int centerY = d.height/2;
	    final int rad = (Math.min(d.height, d.width)* 8) /20;
	    final int lineLen = rad/8;
	    final int tail = ( 7 * lineLen ) /10 ;
	    final int nose = ( 3 * lineLen ) /10 ;
	    final int wing = lineLen/2;
	    final int vecLen = 3 * lineLen;
	    final int arrow = lineLen/3;
	    final int holdX = centerX - vecLen;
	    final int holdY = centerY - vecLen/2;
	    final int holdLen = 2*vecLen;
	    final int vorRad = 3;
	    final double inboundRadians = degreesToRadians(heading+180);
	    final double holdRadians = degreesToRadians(radial);
	    String hString = "Heading: " + headingString(heading); 

	    if (!overlay) {
		g2.setColor(Color.white);
		g2.fillRect(0, 0, d.width, d.height);
		g2.setColor(Color.black);
		g2.drawString(hString, insets.left, 
			d.height - insets.bottom  - 2);
	    }

	    if ( !northIsUp || overlay) {
		final double reverseIt = degreesToRadians(-heading);

		// Undo the plane rotation if north is not up.
		planeXform.rotate(reverseIt, centerX, centerY);
		holdXform.rotate(reverseIt, centerX, centerY);
	    }

	    if ( !overlay ) {
		// The plane.
		planeXform.rotate(inboundRadians, centerX, centerY);

		g2.setTransform(planeXform);
		g2.drawLine(centerX, centerY-rad-tail, centerX, 
			centerY-rad + nose);
		g2.drawLine(centerX-wing, centerY-rad, centerX+wing, 
			centerY-rad);
		// Arrow
		g2.setColor(Color.red);
		g2.drawLine(centerX, centerY-rad + wing, 
		    centerX, centerY - rad + vecLen);
		g2.drawLine(centerX- arrow, centerY-rad + vecLen - arrow, 
		    centerX, centerY-rad + vecLen);
		g2.drawLine(centerX+ arrow, centerY-rad + vecLen - arrow, 
		    centerX, centerY-rad + vecLen);
	    }

	    // If the hold is left turns, translate the origin to the center
	    // of the JPanel, reflect across the y-axis and restore the origin.
	    if ( leftTurns ) {
		AffineTransform flip = 
		    AffineTransform.getTranslateInstance(centerX, 0);
		flip.concatenate(new AffineTransform(-1, 0, 0, 1, 0, 0));
		flip.concatenate(
		    AffineTransform.getTranslateInstance(-centerX, 0));
		holdXform.concatenate(flip);
		// Because of the y -> -y switch the rotation angle has changed
		// sign.
		holdXform.rotate(-holdRadians, centerX, centerY);
	    }
	    else holdXform.rotate(holdRadians, centerX, centerY);

	    // Draw the hold
	    g2.setTransform(holdXform);
	    g2.setColor(overlay? Color.cyan : Color.blue);
	    // Arc angle parameters are degrees - wierd.
	    g2.drawArc(holdX, holdY, vecLen, vecLen, 0, -180);
	    g2.drawArc(holdX, holdY - holdLen, vecLen, vecLen, 180, -180);
	    g2.drawLine(centerX, centerY, centerX, centerY - holdLen);
	    g2.drawLine(centerX-arrow, centerY-arrow-vorRad, 
		centerX, centerY-vorRad);
	    g2.drawLine(centerX+arrow, centerY-arrow-vorRad, 
		centerX, centerY-vorRad);
	    g2.drawLine(holdX, centerY, holdX, centerY - holdLen);
	    // Arrow for hold direction
	    g2.drawLine(holdX-arrow, centerY-holdLen +arrow, 
		holdX, centerY-holdLen );
	    g2.drawLine(holdX+arrow, centerY-holdLen +arrow, 
		holdX, centerY-holdLen );
	    if ( !overlay ) {
		g2.setColor(Color.black);
		g2.fillOval(centerX-vorRad, centerY-vorRad, 2 * vorRad, 
			2 * vorRad);
	    }
	    g2.setTransform(restoreXform);
	}

	/**
	 * Change the display mode to the next one.
	 */
	protected void nextDisplay() {
	    switch (display) {
		case HI:
		    display = DisplayType.DIAGRAM;
		    break;
		case DIAGRAM:
		    display = DisplayType.BOTH;
		    break;
		case BOTH:
		    display = DisplayType.HI;
		    break;
	    }
	}
	/**
	 * Return the prompt for the next display.
	 */
	protected String promptString() {
	    switch (display) {
		case HI:
		    return "Show Diagram";
		case DIAGRAM:
		    return "Show HI/Diagram Overlay";
		case BOTH:
		    return "Show Heading Indicator";
	    }
	    return "Can't get here...";
	}
    }

    /**
     * A question is pending, when the button is hit, calculate the time spend
     * working, and display it and the correct hold entry.  Then set the
     * button's action listener to the idleQuiz handler.
     */
    protected class PendingQuestion implements ActionListener {
	PendingQuestion() { }
	public void actionPerformed(ActionEvent e) {
	    StringWriter s = new StringWriter();
	    PrintWriter out = new PrintWriter(s);
	    long elapsed = (System.currentTimeMillis() - started) / 1000;
	    JButton b = (JButton) e.getSource();

	    out.println(clearance());
	    out.println();
	    out.println(entryType());
	    out.println();
	    out.println("Elapsed time: " + elapsed + " seconds");
	    msg.setText(s.toString());
	    b.removeActionListener(this);
	    b.addActionListener(idleQuiz);
	    b.setText("Next Hold");
	    repaint();
	}
    }

    /**
     * A question is not pending, when the button is hit, calculate a new hold
     * and display it.  Then set the button's action listener to the
     * pendingQuestion handler.
     */
    protected class IdleQuiz implements ActionListener {
	IdleQuiz() { }
	public void actionPerformed(ActionEvent e) {
	    JButton b = (JButton) e.getSource();

	    newHold();
	    msg.setText(clearance());
	    b.removeActionListener(this);
	    b.addActionListener(pendingQuestion);
	    b.setText("Show Entry");
	    repaint();
	}
    }

    /**
     * Switch between the HI and diagram.  Switch in the other panel, change
     * the button text and switch listeners.
     * */
    protected class SwitchView implements ActionListener {
	protected HQPanel comp;	    // The HQPanel to control

	SwitchView(HQPanel c) { comp = c; }

	public void actionPerformed(ActionEvent e) {
	    JButton b = (JButton) e.getSource();
	    comp.nextDisplay();
	    b.setText(comp.promptString());
	    repaint();
	}
    }

    /** This ItemListner ties the state of a checkbox to teh state of a
     * display's "North is up" attribute */
    protected class NorthIsUp implements ItemListener {
	protected HQPanel diag;
	/** Constructor */
	public NorthIsUp(HQPanel d) { diag = d; }

	public void itemStateChanged(ItemEvent e) {
	    diag.setNorthIsUp(e.getStateChange() == ItemEvent.SELECTED);
	}
    };


    /**
     * Convert degrees to radians.  Completely trivial.
     * @param d dgerees
     * @return radians
     */
    double degreesToRadians(double deg) { return deg * Math.PI / 180.0; }

    /**
     * Make sure the heading is greater than 0 and at most 360.  Zero headings
     * become 360 headings.
     * @param h the raw heading
     * @return the fixed heading
     */
    protected int fixHeading(int h) { 
	while ( h < 0 ) h+= 360;
	if ( h > 360 ) h = h % 360;
	if (h == 0 ) return 360;
	return h;
    }

    /**
     * Return a printable version of the heading: 3 digits, padded with zeroes.
     * The heading should be bigger than 0 and at most 360 before this call.
     * @param h the heading to print
     * @return the printable heading
     */
    protected String headingString(int h) {
	StringBuffer sb = new StringBuffer(Integer.toString(h));
	while (sb.length() != 3 ) sb.insert(0, '0');
	return (sb.toString());
    }

    /**
     * True if the third parameter is between the first two inclusive, taken as
     * modulo values.  No bounds checking is done, so GIGO.
     * @param bottom bottom of the test range
     * @param top of the test range
     * @param x test value
     * @return true if x is between b and t
     */
    protected boolean between(int bottom, int top, int x) {
	if ( bottom < top ) return (bottom <= x) && ( x <= top);
	else return (x <= top ) || ( x >= bottom );
    }

    /**
     * Return the FAA recommended entry type for this hold.  The return value
     * is a string with all the entry types that the FAA would accept as
     * entries (and there may be two).
     * @return entry types
     */
    public String entryType() {
	StringBuffer sb = new StringBuffer();		// Return value
	int inboundRadial = fixHeading(heading + 180);	// Radial from the fix 
							// on which the
							// aircraft is
							// approaching.
	int pd, td, pt;					// Boundaries between
							// entry types as
							// radials form the
							// holding fix.

	if ( leftTurns ) {
	    pd = fixHeading(radial+70);
	    pt = fixHeading(radial-180);
	    td = fixHeading(radial-110);
	    if ( between(td, pd, inboundRadial) ) 
		sb.append("direct");
	    if ( between(pd, pt, inboundRadial) ) 
		sb.append((sb.length() > 0) ? " parallel" : "parallel");
	    if ( between(pt, td, inboundRadial) ) 
		sb.append((sb.length() > 0) ? " teardrop" : "teardrop");
	}
	else {
	    pd = fixHeading(radial-70);
	    pt = fixHeading(radial+180);
	    td = fixHeading(radial+110);
	    if ( between(pd, td, inboundRadial) ) 
		sb.append("direct");
	    if ( between(td, pt, inboundRadial) ) 
		sb.append((sb.length() > 0) ? " teardrop": "teardrop");
	    if ( between(pt, pd, inboundRadial) ) 
		sb.append((sb.length() > 0) ? " parallel": "parallel");
	}
	return sb.toString();
    }

    public String inboundCourse() { return headingString(heading); }

    protected String sector() {
	if ( between(338, 23, radial) ) return "north"; 
	else if ( between(23, 68, radial) ) return "northeast";
	else if ( between(68, 113, radial) ) return "east";
	else if ( between(113, 158, radial) ) return "southeast";
	else if ( between(158, 203, radial) ) return "south";
	else if ( between(203, 248, radial) ) return "southwest";
	else if ( between(248, 293, radial) ) return "west";
	else return "northwest";
    }

    /**
     * A clearance for this hold (as a VOR hold).
     * @return the clearance
     */
    public String clearance() {
	StringWriter sw = new StringWriter();
	PrintWriter out = new PrintWriter(sw);
	Random rand = new Random();
	String where = null;

	out.println("hold " + sector() + " of the XYZ VOR");
	out.println("on the " + headingString(radial) + " radial");
	out.println("at " + alt);
	if ( leftTurns) out.println("left turns");
	out.print("expect further clearance in " + efc + " minutes");
	return sw.toString();
    }

    /**
     * Pick new random values for the hold.
     */
    void newHold() {
	started = System.currentTimeMillis();
	heading = fixHeading(rand.nextInt(72) * 5);
	radial = fixHeading(rand.nextInt(72) * 5);
	leftTurns = rand.nextBoolean();
	alt = (rand.nextInt(9) + 1) * 1000;
	efc = (rand.nextInt(5) + 1) *5;
    }

    void buildUI() {
	GridBagLayout gb = (GridBagLayout) new GridBagLayout();
	GridBagConstraints c = new GridBagConstraints();
	JPanel panel = new JPanel(gb);
	panel.setBorder(
	    BorderFactory.createMatteBorder(2, 2, 2, 2, Color.white.darker()));

	// Set each Component's position and add it.  Note that the view is in
	// position 0 and that the same constraints are applied in SwitchView.
	c.gridheight = 1;
	c.gridwidth = 1;
	c.weighty = 1;
	c.weightx = 1;
	c.gridx=0;
	c.gridy=0;
	c.fill = GridBagConstraints.BOTH;
	gb.setConstraints(hqPanel, c);
	panel.add(hqPanel);
	c.gridx=0;
	c.gridy=1;
	c.weighty = 0;
	c.fill = GridBagConstraints.HORIZONTAL;
	gb.setConstraints(b1, c);
	panel.add(b1);
	c.weightx = 0;
	c.weighty = 0;
	c.gridwidth = 1;
	c.gridx=1;
	c.gridy=0;
	c.fill = GridBagConstraints.BOTH;
	gb.setConstraints(msg, c);
	panel.add(msg);
	c.gridx=1;
	c.gridy=1;
	c.weightx = 0;
	c.weighty = 0;
	c.fill = GridBagConstraints.HORIZONTAL;
	gb.setConstraints(b2, c);
	panel.add(b2);
	c.gridx=0;
	c.gridy=2;
	c.weightx = 0;
	c.weighty = 0;
	c.fill = GridBagConstraints.HORIZONTAL;
	gb.setConstraints(up_p, c);
	panel.add(up_p);
	setContentPane(panel);
	panel.validate();
    }

    /**
     * Straightforward constructor.
     */
    public HoldQuiz() {
	super();
    }

    /**
     * The real constructor, because this is an applet.  Initialization is
     * straightforward with the possible exception of the GridBagConstraints.
     */
    public void init() {
	// Basic initialization
	rand = new Random();
	newHold();
	hqPanel  = new HQPanel();

	msg = new JTextArea(clearance(), 10, 25);
	msg.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
	msg.setMinimumSize(new Dimension(250, 250));
	
	idleQuiz = new IdleQuiz();
	pendingQuestion = new PendingQuestion();
	panel = new SwitchView(hqPanel);

	// Now the grid bag tricks.
	b1 = new JButton(hqPanel.promptString());
	b1.addActionListener(panel);

	b2 = new JButton("Show Entry");
	b2.addActionListener(pendingQuestion);

	up_p = new JCheckBox("North is up", true);
	up_p.addItemListener(new NorthIsUp(hqPanel));

	buildUI();
    }

    /**
     * Open a JFrame, create a new Quiz object, make it the frame's
     * contentPane, and make the Frame visible.
     * @param args command line params, ignored.
     */
    public static void main(String[] args) {
	JFrame frame = new JFrame("test");
	HoldQuiz hq = new HoldQuiz();
	hq.init();

	frame.setContentPane(hq);
	frame.pack();
	frame.addWindowListener(new WindowAdapter() {
	    public void windowClosing(WindowEvent e) {
		System.exit(0);
	    }
	});
	frame.setVisible(true);
    }
}

