import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import java.util.*;
import sun.audio.AudioPlayer;
import gnu.regexp.*;

/*

--------------------------------------------------------------------------------

    Morse Code Translator Java Applet
    Copyright (C) 1999  Stephen C Phillips
    Copyright (C) 2001  Michael R Ditto

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

--------------------------------------------------------------------------------

    You can contact Stephen by email at: steve@scphillips.com
    You can contact Michael by email at: ford@omnicron.com

--------------------------------------------------------------------------------

Version MD.0.1	2001-3-26
	Converted to Swing.
Version MD.0.0	2001-3-24
	Update for AWT1.1.
	Add "Stop" button.
	Encapsulate the sun.audio dependency so it still works in browsers
		without sound.
	Add random quotation option (fetches a quote from a URL).
	Add random words option (fetches a series of words from a URL).
	Translate `` and '' into " since they are used in the random quotes.
	Use element compression for slow speeds (sends individual characters at
		12WPM, with the inter-character spacing stretched out to keep
		the overall WPM as specified).
Version 1.0.1	1999-8-23
	Minor correction to translate action button
Version 1.0.0	1999-8-10
	First public release

--------------------------------------------------------------------------------

This is my first Java program so if you have any constructive comments
then please let me know!  I will be tidying it up shortly...
-Stephen

--------------------------------------------------------------------------------

*/

public class SMorse extends JApplet implements ActionListener, ItemListener {
    JTextArea input;
    JLabel lettersLabel;
    JTextField letters;
    JTextArea output;
    JTextArea status;
    JButton transButton;
    JButton playButton, stopButton;
    JComboBox sourceChoice;
    JComboBox wpmChoice;
    Hashtable hMorse;
    Hashtable hText;
    byte [] sample = null;
    int sPos;
    int timeunit;

    public void init() {
	JComponent contentPane = (JComponent)getContentPane();

	contentPane.setLayout(
	    new BoxLayout(contentPane, BoxLayout.Y_AXIS));
	contentPane.setBorder(
	    BorderFactory.createTitledBorder(
		BorderFactory.createEmptyBorder(),
		"Morse Code Translator",
		TitledBorder.CENTER,
		TitledBorder.TOP,
		new Font("SansSerif", Font.BOLD, 16)));

	input =
	    new JTextArea("Type your text or dots and dashes here.", 4, 80);
	input.setLineWrap(true);
	input.setWrapStyleWord(true);
	input.setEditable(true);
	input.selectAll();
	JScrollPane inputScrollPane = new JScrollPane(input);
	inputScrollPane.setBorder(
	    BorderFactory.createCompoundBorder(
		BorderFactory.createCompoundBorder(
		    BorderFactory.createTitledBorder("Input"),
		    BorderFactory.createEmptyBorder(5, 5, 5, 5)),
		inputScrollPane.getBorder()));
	contentPane.add(inputScrollPane);

	output = new JTextArea(2, 80);
	output.setEditable(false);
	input.setLineWrap(true);
	JScrollPane outputScrollPane = new JScrollPane(output);
	outputScrollPane.setBorder(
	    BorderFactory.createCompoundBorder(
		BorderFactory.createCompoundBorder(
		    BorderFactory.createTitledBorder("Output"),
		    BorderFactory.createEmptyBorder(5, 5, 5, 5)),
		outputScrollPane.getBorder()));
	contentPane.add(outputScrollPane);

	String [] sourceChoices =
	    { "Typed Input", "Random Quotation",
	      "Random Words using these letters:", };
	sourceChoice = new JComboBox(sourceChoices);
	sourceChoice.setSelectedIndex(0);
	sourceChoice.addItemListener(this);

	letters = new JTextField("etianmos", 10);
	letters.setEditable(true);
	letters.setEnabled(false);
	letters.getCaret().setDot(Integer.MAX_VALUE);

	JPanel sourcePanel = new JPanel();
	sourcePanel.add(new JLabel("from"));
	sourcePanel.add(sourceChoice);
	sourcePanel.add(letters);
	contentPane.add(sourcePanel);

	transButton = new JButton("Translate");
	transButton.addActionListener(this);

	playButton = new JButton("Play");
	playButton.addActionListener(this);

	String [] wpmChoices =
	    { "5", "10", "13", "15", "18", "20", "25", "30", "35", "40", };
	wpmChoice = new JComboBox(wpmChoices);
	wpmChoice.setSelectedIndex(0);

	stopButton = new JButton("Stop");
	stopButton.setEnabled(false);
	stopButton.addActionListener(this);

	JPanel actionPanel = new JPanel();
	actionPanel.setLayout(
	    new BoxLayout(actionPanel, BoxLayout.X_AXIS));
	actionPanel.setBorder(BorderFactory.createEtchedBorder());

	actionPanel.add(transButton);
	actionPanel.add(Box.createRigidArea(new Dimension(10, 0)));
	actionPanel.add(Box.createHorizontalGlue());
	actionPanel.add(playButton);
	actionPanel.add(new JLabel("at"));
	actionPanel.add(wpmChoice);
	actionPanel.add(new JLabel("WPM"));
	actionPanel.add(Box.createRigidArea(new Dimension(10, 0)));
	actionPanel.add(stopButton);
	contentPane.add(actionPanel);

	status = new JTextArea(1, 80);
	status.setLineWrap(true);
	status.setEditable(false);
	status.setBackground(playButton.getBackground());
	status.setForeground(playButton.getForeground());
	contentPane.add(status);

	hMorse = new Hashtable(46);
	hText = new Hashtable(46);

	hMorse.put(new Character('A'), ".-");
	hMorse.put(new Character('B'), "-...");
	hMorse.put(new Character('C'), "-.-.");
	hMorse.put(new Character('D'), "-..");
	hMorse.put(new Character('E'), ".");
	hMorse.put(new Character('F'), "..-.");
	hMorse.put(new Character('G'), "--.");
	hMorse.put(new Character('H'), "....");
	hMorse.put(new Character('I'), "..");
	hMorse.put(new Character('J'), ".---");
	hMorse.put(new Character('K'), "-.-");
	hMorse.put(new Character('L'), ".-..");
	hMorse.put(new Character('M'), "--");
	hMorse.put(new Character('N'), "-.");
	hMorse.put(new Character('O'), "---");
	hMorse.put(new Character('P'), ".--.");
	hMorse.put(new Character('Q'), "--.-");
	hMorse.put(new Character('R'), ".-.");
	hMorse.put(new Character('S'), "...");
	hMorse.put(new Character('T'), "-");
	hMorse.put(new Character('U'), "..-");
	hMorse.put(new Character('V'), "...-");
	hMorse.put(new Character('W'), ".--");
	hMorse.put(new Character('X'), "-..-");
	hMorse.put(new Character('Y'), "-.--");
	hMorse.put(new Character('Z'), "--..");
	hMorse.put(new Character('1'), ".----");
	hMorse.put(new Character('2'), "..---");
	hMorse.put(new Character('3'), "...--");
	hMorse.put(new Character('4'), "....-");
	hMorse.put(new Character('5'), ".....");
	hMorse.put(new Character('6'), "-....");
	hMorse.put(new Character('7'), "--...");
	hMorse.put(new Character('8'), "---..");
	hMorse.put(new Character('9'), "----.");
	hMorse.put(new Character('0'), "-----");
	hMorse.put(new Character('.'), ".-.-.-");
	hMorse.put(new Character('!'), "-.-.--");
	hMorse.put(new Character(','), "--..--");
	hMorse.put(new Character(':'), "---...");
	hMorse.put(new Character('?'), "..--..");
	hMorse.put(new Character('\''), ".----.");
	hMorse.put(new Character('`'), ".----."); // treat ` as '
	hMorse.put(new Character('-'), "-....-");
	hMorse.put(new Character('/'), "-..-.");
	hMorse.put(new Character('('), "-.--.-");
	hMorse.put(new Character(')'), "-.--.-");
	hMorse.put(new Character('"'), ".-..-.");
	hMorse.put(new Character('='), "-...-");
	hMorse.put(new Character('+'), ".-.-.");
	hMorse.put(new Character(';'), "-.-.-.");
	hMorse.put(new Character('_'), "..--.-");
	hMorse.put(new Character('$'), "...-..-");
	hMorse.put(new Character('@'), ".--.-.");

	Enumeration list = hMorse.keys();
	while (list.hasMoreElements()) {
	    Character c = (Character)list.nextElement();
	    String s = (String)hMorse.get(c);
	    hText.put(s, c);
	}
    }

    public void start() {
	if (sourceChoice.getSelectedIndex() == 0)
	    input.requestFocus();
    }

    public void itemStateChanged(ItemEvent event) {
	if (event.getSource() == sourceChoice &&
	    event.getStateChange() == ItemEvent.SELECTED) {
	    boolean enaInput = sourceChoice.getSelectedIndex() == 0;
	    boolean enaLetters = sourceChoice.getSelectedIndex() == 2;
	    input.setEditable(enaInput);
	    if (enaInput) {
		input.selectAll();
		input.requestFocus();
	    }
	    letters.setEnabled(enaLetters);
	    if (enaLetters)
		letters.requestFocus();
	}
    }

    public void actionPerformed(ActionEvent event) {
	String morse = null;

	quiet();
	stopButton.setEnabled(false);

	if (event.getSource() == transButton ||
	    event.getSource() == playButton) {

	    switch (sourceChoice.getSelectedIndex()) {
	    case 0:
		status.setText("translating...");
		if (isText(input.getText())) {
		    morse = toMorse(input.getText());
		    output.setText(morse);
		} else {
		    morse = input.getText();
		    output.setText(toText(morse));
		}
		break;
	    case 1:
		status.setText("fetching random text...");
		input.setText(randomText(null));
		status.setText("translating...");
		morse = toMorse(input.getText());
		output.setText(morse);
		break;
	    case 2:
		status.setText("fetching random text...");
		input.setText(randomText(letters.getText()));
		status.setText("translating...");
		morse = toMorse(input.getText());
		output.setText(morse);
		break;
	    }
	    status.setText("translated");

	    if (event.getSource() == playButton) {
		byte[] sample = makeSample(morse);
		status.setText("playing...");
		try {
		    playButton.setEnabled(false);
		    stopButton.setEnabled(true);
		    (new Sound(sample)).play();
		    playButton.setEnabled(true);
		} catch (Exception ex) {
		    status.setText(ex.toString());
		    playButton.setEnabled(true);
		}
	    }
	}
    }

    /**************************************************************/
    /*	Random Text Acquisition					  */
    /**************************************************************/

    private String randomText(String letters) {
	try {
	    URL textURL =
		new URL(getCodeBase(),
			("/cgi-bin/randomtext" +
			 (letters == null ? "" : "?" + letters)));
	    InputStreamReader sr =
		new InputStreamReader((InputStream)textURL.getContent(),
				      "UTF8");
	    StringBuffer b = new StringBuffer();
	    char[] buf = new char[100];
	    int n;
	    while ((n = sr.read(buf)) > 0) {
		b.append(buf, 0, n);
	    }
	    return b.toString();
	} catch (Exception ex) {
	    ex.printStackTrace();
	    status.setText(ex.toString());
	    return "error";
	}
    }

    /**************************************************************/
    /*	Text Processing						  */
    /**************************************************************/

    boolean isText(String sText) {
	boolean matches = false;
	try {
	    RE morseRE = new RE("^[._/ \\|-]*$");
	    matches = morseRE.isMatch(sText);
	}
	catch (REException arg) { }
	return !matches;
    }

    String toMorse(String sText) {
	/*
	  tr/a-z/A-Z/; #lowercase
	  tr/ / /s; #sqeeze spaces
	  s/^ *(.*?) *$/$1/; #chop start and end of ' '
	  s# #~ #g; #mark word boundaries (with non-Morse character)
	  s#([A-Z0-9.!,:?'`/()\"=+;_$@ -])#$toMorse{$1}.' '#eg; #put Morse in
	  s#~#/#g; #re-mark word boundaries
	*/

	StringBuffer sbMorse = new StringBuffer();

	sText = sText.toUpperCase();
	try {
	    RE sub1 = new RE("\\s+");
	    RE sub2 = new RE("^\\s+");
	    RE sub3 = new RE("\\s+$");
	    RE sub5 = new RE("[^A-Z0-9.!,:?'`/()\"=+;_$@ -]");
	    RE sub4 = new RE("[\'`]{2}");
	    sText = sub1.substituteAll(sText, " ");
	    sText = sub2.substitute(sText, "");
	    sText = sub3.substitute(sText, "");
	    sText = sub4.substituteAll(sText, "\"");
	    sText = sub5.substituteAll(sText, "");
	    input.setText(sText);

	    for (int i = 0; i < sText.length(); i++) {
		char c = sText.charAt(i);
		Character ch = new Character(c);
		if (Character.isWhitespace(c)) {
		    sbMorse.append("/ ");
		} else {
		    sbMorse.append(hMorse.get(ch));
		    sbMorse.append(' ');
		}
	    }
	} catch (REException arg) {
	    //input.setText(arg.getMessage());
	    input.setText("error " + arg);
	}

	return sbMorse.toString();
    }

    public String toText(String sMorse) {
	/*
	  tr/ / /s; #sqeeze space
	  s#( ?/ ?)+# /#g; #deal with multiple and space-padded '/'
	  s#^[ /]*##; #chop start of '/' and ' '
	  s#[ /]*$# #; #chop end of same, leaving one ' '
	  s#/#~#g; #mark word boundaries (with non-Morse character)
	  s/([-.]+) /$fromMorse{"$1"}/eg; #put in text
	  s/~/ /g; #replace word boundaries
	  s#\(([^(]*)\(#\($1\)#g; #match brackets
	*/
	StringBuffer sbText = new StringBuffer();

	try {
	    RE sub1 = new RE("\\|");  // for '/'
	    RE sub2 = new RE("_");  // for '-'
	    RE sub3 = new RE("\\s+");  // for ' '
	    RE sub4 = new RE("( ?/ ?)+");  // for ' / '
	    RE sub5 = new RE("^[ /]*");	 // for ''
	    RE sub6 = new RE("[ /]*$");	 // for ''
	    sMorse = sub1.substituteAll(sMorse, "/");
	    sMorse = sub2.substituteAll(sMorse, "-");
	    sMorse = sub3.substituteAll(sMorse, " ");
	    sMorse = sub4.substituteAll(sMorse, " / ");
	    sMorse = sub5.substitute(sMorse, "");
	    sMorse = sub6.substitute(sMorse, "");
	    input.setText(sMorse);
	    sMorse = sMorse + " ";

	    RE sub7 = new RE("^.*? ");	// for ''  (rest of string)
	    RE sub8 = new RE(" .*$");  // for ''  (first word)

	    while (sMorse.length() != 0) {
		String letter = sub8.substitute(sMorse, "");
		sMorse = sub7.substitute(sMorse, "");
		if (letter.equals("/")) {
		    sbText.append(" ");
		} else if (hText.get(letter) == null) {
		    sbText.append("*");
		} else {
		    sbText.append(hText.get(letter));
		}
	    }
	    // need to do parentheses matching!

	} catch (REException arg) {
	    input.setText(arg.toString());
	}

	return sbText.toString();
    }

    /**************************************************************/
    /*	Sound Processing					  */
    /**************************************************************/

    Sound playing = null;

    class Sound {
	private InputStream stream = null;
	byte[] sample;

	Sound(byte[] sample) {
	    this.sample = sample;
	}

	void play() {
	    quiet();
	    stream = new ByteArrayInputStream(sample);
	    AudioPlayer.player.start(stream);
	    playing = this;
	}

	void stop() {
	    if (stream != null) {
		AudioPlayer.player.stop(stream);
		stream = null;
	    }
	    if (playing == this)
		playing = null;
	}
    }

    void quiet() {
	if (playing != null) {
	    status.setText("");
	    playing.stop();
	}
    }

    byte [] makeSample(String morse) {
	int wpm = Integer.parseInt((String)wpmChoice.getSelectedItem());
	final int ditLen = 1;
	final int dahLen = 3;
	final int wordLen = 50;
	final int charPause = 3;
	final int wordPause = 7;
	char c;
	float compression;
	int extra;

	// milliseconds per dit
	timeunit = 1000*60/(wpm*wordLen);
	if (timeunit > 100) {
	    // If the characters are to be sent slowly (less than 12wpm)
	    // compress the elements, and stretch the gap between characters
	    // to keep the wpm the same.
	    compression = (float)timeunit / (float)100.0 - 1;
	    timeunit = 100;
	} else {
	    compression = 0;
	}

	int length = 0;
	extra = 0;
	for (int i = 0; i < morse.length(); ++i) {
	    c = morse.charAt(i);
	    switch (c) {
	    case '.':
		extra += ditLen + ditLen;
		length += ditLen + ditLen;
		break;
	    case '-':
		extra += dahLen + ditLen;
		length += dahLen + ditLen;
		break;
	    case ' ':
		extra += charPause - ditLen;
		length += charPause - ditLen + (int)(extra * compression);
		extra = 0;
		break;
	    case '/':
		extra += wordPause - ditLen;
		length += wordPause - ditLen + (int)(extra * compression);
		extra = 0;
		break;
	    }
	}

	length *= 8 * timeunit;

	sample = new byte[length];
	sPos = 0;

	extra = 0;
	for (int i = 0; i < morse.length(); ++i) {
	    c = morse.charAt(i);
	    switch (c) {
	    case '.':
		extra += ditLen + ditLen;
		addSound(ditLen);
		addSilence(ditLen);
		break;
	    case '-':
		extra += dahLen + ditLen;
		addSound(dahLen);
		addSilence(ditLen);
		break;
	    case ' ':
		extra += charPause - ditLen;
		addSilence(charPause - ditLen + (int)(extra * compression));
		extra = 0;
		break;
	    case '/':
		extra += wordPause - ditLen;
		addSilence(wordPause - ditLen + (int)(extra * compression));
		extra = 0;
		break;
	    }
	}

	return sample;
    }

    private void addSound(int units) {
	for (int i = 0; i < units*timeunit; ++i) {
	    sample[sPos++] = (byte)0xA7;
	    sample[sPos++] = (byte)0x81;
	    sample[sPos++] = (byte)0xA7;
	    sample[sPos++] = (byte)0;
	    sample[sPos++] = (byte)0x59;
	    sample[sPos++] = (byte)0x7F;
	    sample[sPos++] = (byte)0x59;
	    sample[sPos++] = (byte)0;
	}
    }

    private void addSilence(int units) {
	int oPos = sPos;
	sPos += 8*units*timeunit;
	Arrays.fill(sample, oPos, sPos, (byte)0);
    }
}
