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 display */
    HeadingIndicator hi;
    /** The holding diagram*/
    Diagram diagram;
    /** 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;
    /** Active when the HI is displayed */
    SwitchView showingHI;
    /** Active when the diagram is displayed */
    SwitchView showingDiagram;

    protected class HQPanel extends JPanel {
	/**
	 * Simple constructor.  Fill in a preferred size and a border.
	 */
	HQPanel() { 
	    super(); 
	    setPreferredSize(new Dimension(250,250)); 
	    setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
	}
    }

    /**
     * This class displays a heading indicator reading the curent problem's
     * heading.
     */
    protected class HeadingIndicator extends HQPanel {
    
	/**
	 * Simple constructor. 
	 */
	HeadingIndicator() { super(); }

	/**
	 * Redraw this component.  Basically straight lines are drawn and
	 * labelled, but the transform is slowly rotated around to create a
	 * dial.
	 * @param g the current graphics context
	 */
	public void paintComponent(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);
	}
    }

    /**
     * Class to draw the diagram for overview.
     */
    class Diagram extends HQPanel {
	/** True if the aircraft changes heading */
	protected boolean northIsUp;
	/**
	 * Simple constructor.
	 */
	Diagram() { super(); northIsUp=true; }

	/** Accessors for the "north is up property" */
	public boolean setNorthIsUp(boolean v) { return northIsUp = v; }
	public boolean getNorthIsUp() { return northIsUp;}

	/**
	 * Draw the component.  Again, liberal use of transforms gets the most
	 * out of a few simple drawing operations.
	 * @param g the graphics context
	 */
	public void paintComponent(Graphics g) {
	    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); 

	    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 ) {
		final double reverseIt = degreesToRadians(-heading);

		// Undo the plane rotation if north is not up.
		planeXform.rotate(reverseIt, centerX, centerY);
		holdXform.rotate(reverseIt, centerX, centerY);
	    }
	    // 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(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 );
	    g2.setColor(Color.black);
	    g2.fillOval(centerX-vorRad, centerY-vorRad, 2 * vorRad, 2 * vorRad);
	    g2.setTransform(restoreXform);
	}
    }


    /**
     * 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 JComponent comp;   // Component to put into the first position
	protected String text;	    // New button text
	protected ActionListener otherListener; // Listener

	SwitchView(JComponent c, String t, ActionListener ol) {
	    comp = c; text = t; otherListener = ol;
	}

	/**
	 * Set the otherListener.
	 */
	void setOtherListener(ActionListener ol) { otherListener = ol; }

	public void actionPerformed(ActionEvent e) {
	    JButton b = (JButton) e.getSource();
	    Container cp = getContentPane();
	    GridBagLayout gb = (GridBagLayout) cp.getLayout();
	    GridBagConstraints c = gb.getConstraints(cp.getComponent(0));

	    b.removeActionListener(this);
	    b.addActionListener(otherListener);
	    b.setText(text);
	    gb.setConstraints(comp, c);

	    cp.remove(0);
	    cp.add(comp, 0);
	    cp.validate();
	    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 Diagram diag;
	/** Constructor */
	public NorthIsUp(Diagram 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(hi, c);
	panel.add(hi);
	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();
	hi = new HeadingIndicator();
	diagram  = new Diagram();

	msg = new JTextArea(clearance(), 10, 25);
	msg.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
	msg.setMinimumSize(new Dimension(250, 250));
	
	idleQuiz = new IdleQuiz();
	pendingQuestion = new PendingQuestion();
	showingDiagram = new SwitchView(hi, "Show Diagram", null);
	showingHI = 
	    new SwitchView(diagram, "Show Heading Indicator", showingDiagram);
	showingDiagram.setOtherListener(showingHI);

	// Now the grid bag tricks.
	b1 = new JButton("Show Diagram");
	b1.addActionListener(showingHI);

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

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

	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);
    }
}
