import java.applet.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
import java.util.TimerTask;
import javax.swing.*;

public class morseTrainer extends Applet
{
	private static final long serialVersionUID = 1L;
	
	/*
	 * Constants
	 */
	private final String TONE_CLIP_FILE = "tone.wav";
	private final String DING_CLIP_FILE = "ding.wav";
	private final String BUZZ_CLIP_FILE = "buzz.wav";
	private final long DEFAULT_DOT_DURATION = 200;
	private final long MAX_DASH_DURATION = 1500;
	private final long END_INPUT_DELAY = 50;
	
	/*
	 * Audio Clips
	 */
	public static AudioClip dotClip;
	private AudioClip dingClip;
	private AudioClip buzzClip;
	
	public static boolean isPlaying=false;
	public static long toneStartTime;
	public static long lastKeyUp;
	
	/*
	 * The [input] string holds the morse code the user has input so far
	 * this is cleared for each round
	 */
	private String input = new String();
	
	/*
	 * Labels
	 */
	private JLabel lblChar = new JLabel(); // displays the letter the user must enter
	private JLabel lblInput = new JLabel(); // displays the morse code the user has entered thus far
	private JLabel lblResult = new JLabel(); // displays "correct" or "incorrect"
	
	private keyHandler myKeyHandler = new keyHandler();
	
	/*
	 * When no input is received for some amout of time [END_INPUT_DELAY]
	 * then submit the morse code that has been input
	 * to see if the user got it right 
	 */
	private java.util.Timer endInputTimer = new java.util.Timer();	
	
	private JSlider sldDotDelay; // slider lets the user define threashold between dot and dash
	
	/*
	 * Parallel arrays to look up success
	 * look up the user's input in [codes], if the corresponding entry in [letters] (by same index)
	 * matches the contents of the lable (lblChar), then success
	 */
	private final String[] letters = { 
			"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", 
			"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
			"# 0", "# 1", "# 2", "# 3", "# 4", "# 5", "# 6", "# 7", "# 8", "# 9" };
	private final String[] codes = 
		{
			".-",
			"-...",
			"-.-.",
			"-..",
			".",
			"..-.",
			"--.",
			"....",
			"..",
			".---",
			"-.-",
			".-..",
			"--",
			"-.",
			"---",
			".--.",
			"--.-",
			".-.",
			"...",
			"-",
			"..-",
			"...-",
			".--",
			"-..-",
			"-.--",
			"--..",
			"-----",
			".----",
			"..---",
			"...--",
			"....-",			
			".....",			
			"-....",
			"--...",
			"---..",
			"----.",				
		};
	
	/*
	 * A random number generator to fetch a random entry from [letters] for each round
	 */
	private Random rnd = new Random(System.currentTimeMillis());
	
	/*
	 * init is the entry point for this, or any, applet 
	 */
	public void init()
	{		
		// let me micro-manage the exact location and width/height of each element
		setLayout(null);
		
		// get the size of the applet so that controls' sizes can be set relative to it
		Rectangle r = this.getBounds();

		// add the slider that lets the user set the dot/dash threashold
		addSlider(r);
		
		// set up the input label
		lblInput.setBounds( 15, r.height-50, r.width-15, 50 );
		lblInput.setFont(new Font("Comic Sans", Font.PLAIN, 48));
		add(lblInput);
		
		// set up the result label
		lblResult.setBounds(0,r.height-65,r.width,50);
		lblResult.setFont(new Font("Comic Sans", Font.PLAIN, 24));
		lblResult.setText("");
		lblResult.setHorizontalAlignment(SwingConstants.CENTER);
		add(lblResult);
		
		// set up the character label (the main label)
		lblChar.setText(getRandomLetter());
		lblChar.setBounds(this.getBounds());
		lblChar.setFont(new Font("Comic Sans", Font.PLAIN, 128));
		lblChar.setHorizontalAlignment(SwingConstants.CENTER);		
		add(lblChar);
		
		// load the audio clips
		dotClip = getAudioClip(getCodeBase(), TONE_CLIP_FILE);
		dingClip = getAudioClip(getCodeBase(), DING_CLIP_FILE);
		buzzClip = getAudioClip(getCodeBase(), BUZZ_CLIP_FILE);
		
		// start the timer that will handle the event when the user
		// is done entering morse code
		endInputTimer.scheduleAtFixedRate(new endInputHandler(), 0, END_INPUT_DELAY);
		
		// add a key listener to the applet itself
		// (the same handler will be given to the slider, because it might get focus)
		addKeyListener(myKeyHandler);
	}
	
	private void addSlider(Rectangle r)
	{
		sldDotDelay = new JSlider();
		sldDotDelay.setMinimum(100);
		sldDotDelay.setMaximum(1000);
		sldDotDelay.setValue((int)DEFAULT_DOT_DURATION);
		
		// when the user changes the slider, the slider has focus
		// so it needs the same key listener that the applet does
		sldDotDelay.addKeyListener(myKeyHandler);
		
		sldDotDelay.setBounds(5,15,r.width-5,15);
		add(sldDotDelay);
	}
	
	/*
	 * After input has ended, determine if it was a dot or dash
	 * based on how long the key was held down
	 */
	public void interpretTone(long duration)
	{
		if ( duration < sldDotDelay.getValue() )
		{
			// assume dot
			input += ".";
			lblInput.setText(input);
		}
		else
		{
			// assume dash
			input += "-";
			lblInput.setText(input);
		}
	}
	
	/*
	 * When the [endOfInput] timer has passed AND if there 
	 * hasn't been any input for [MAX_DASH_DURATION] milliseconds
	 * then submit the code inputted so far to see if the user
	 * got it right 
	 */
	public void attemptEndOfInput()
	{
		if ( input.length() < 1 ) return;
		
		long now = System.currentTimeMillis();
		if ( now - lastKeyUp > MAX_DASH_DURATION )
		{
			// no input in the last MAX_DASH_DURATION milliseconds.
			if ( tryMatch(input) )
			{
				// input was correct
				dingClip.play();
				lblResult.setText("Correct!");
			}
			else				
			{
				// input was incorrect
				buzzClip.play();
				lblResult.setText("Incorrect, sorry :(");
			}
			input = "";
			lblInput.setText(input);
			lblChar.setText(getRandomLetter());
		}
	}
	
	private String getRandomLetter()
	{	
		return letters[rnd.nextInt(letters.length)];
	}
	
	/*
	 * Try to match the code the user has entered to the letter
	 * shown on the screen (lblChar)
	 */
	private boolean tryMatch(String inputCode)
	{
		for ( int i=0; i < codes.length; i++)
		{
			if ( codes[i].equals(inputCode) )
			{
				return letters[i].equals(lblChar.getText());
			}
		}
		return false;
	}

	/*
	 * Start the tone when key is pressed down
	 * Stop the tone when key is released
	 */
	private class keyHandler implements KeyListener
	{
		public void keyPressed(KeyEvent arg0) 
		{			
			if ( !isPlaying )					
			{	
				try 
				{ 	
					dotClip.play();
					toneStartTime = System.currentTimeMillis();					
					isPlaying=true;						
				} catch (Exception e) {}
			}
		}
		public void keyReleased(KeyEvent arg0) 
		{
			lastKeyUp = System.currentTimeMillis();
			try
			{
				dotClip.stop();
				long toneDuration = lastKeyUp - toneStartTime;				
				isPlaying=false;
				interpretTone(toneDuration);					
			} catch (Exception e) {} 
		}
		public void keyTyped(KeyEvent arg0) {}		
	}
	
	private class endInputHandler extends TimerTask
	{
		public void run() 
		{
			attemptEndOfInput();
		}	
	}
	
}
