/**
 * Created on Nov 7, 2004
 *
 * Copyright (c) Hans-Christoph Steiner <hans@eds.org>
 *
 * 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.
 *
 * See file LICENSE for further informations on licensing terms.
 *
 * 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.
 */

// TODO new SynthNote for phrase
// TODO add option to record to a wav silently for later playback
// TODO record the performance using JScore to print out a score

package typist;
import java.awt.*;
import java.awt.event.*;
import java.io.*;

import javax.swing.*;

import com.softsynth.jmsl.*;
import com.softsynth.jmsl.jsyn.*;
import com.softsynth.jmsl.util.TuningET;
import com.softsynth.jsyn.util.*;

/**
 * @author hans
 */
public class TypistApplet extends JApplet
		implements
			KeyListener,
			ActionListener {

	JButton resetButton, testButton;
	JCheckBox displayButton;
	JToggleButton recordButton;
	JLabel keyLabel;
	JTextArea displayArea;
	JTextArea typingArea;
	String newline;

	double rootPitch;
	double keyOfPiece;
	boolean keyOfPieceIsSet;

	long[] keyPressedTime; // Java KeyEvent time, in milliseconds
	boolean[] isKeyPressed;

	private double phraseStartTime; // JMSL time, in seconds since program
	// launch
	private double[] keyPressInsData;
	private DimensionNameSpace keyPressNameSpace;

	final static double   FRAME_RATE = 44100.0;
	final static int      FRAMES_PER_BUFFER = 8*1024;
	final static double   TOTAL_TIME = 10.0;
	final static int      TOTAL_FRAMES = (int) (FRAME_RATE * TOTAL_TIME);
	final static int      NUM_REC_CHANNELS = 1;
	StreamRecorder        streamRecorder;
	BufferedOutputStream  outputStream;
	RandomAccessFile      outputFile;
	WAVFileWriter         wavFileWriter;
	boolean				 isRecording;
	
	// JMSL
	JMSLMixerContainer mixer;

	SynthNoteAllPortsInstrument keyPressIns;

	MusicShape keyPressMusicShape;
	MusicShape wordMusicShape;
	MusicShape phraseMusicShape;

	public void init() {
		rootPitch = 60; // middle C
		keyOfPiece = 0; // default to C major
		keyOfPieceIsSet = false;

		keyPressedTime = new long[256];
		isKeyPressed = new boolean[256];

		// JMSL init
		JMSLRandom.randomize();

		buildGUI();
	}

	private void buildGUI() {
		testButton = new JButton("test");
		testButton.setPreferredSize(new Dimension(60, 17));
		testButton.addActionListener(this);

		resetButton = new JButton("reset all");
		resetButton.setPreferredSize(new Dimension(90, 17));
		resetButton.addActionListener(this);

		displayButton = new JCheckBox("text display");
		displayButton.setPreferredSize(new Dimension(110, 17));
		displayButton.addActionListener(this);
		
		recordButton = new JToggleButton("record WAV");
		recordButton.setPreferredSize(new Dimension(120, 20));
		recordButton.addActionListener(this);

		keyLabel = new JLabel("No key set (default is C)",SwingConstants.CENTER);
		keyLabel.setPreferredSize(new Dimension(190, 17));

		JPanel buttonPane = new JPanel();
		buttonPane.setLayout(new FlowLayout(FlowLayout.CENTER));
		buttonPane.add(displayButton);
		buttonPane.add(recordButton);
		buttonPane.add(keyLabel);

		typingArea = new JTextArea();
		typingArea.setEditable(true);
		typingArea.setLineWrap(true);
		typingArea.setWrapStyleWord(true); // wrap at word boundaries, not char
		typingArea.addKeyListener(this);
		JScrollPane typingScrollPane = new JScrollPane(typingArea);
		typingScrollPane.setPreferredSize(new Dimension(575, 425));
		typingScrollPane
				.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

		displayArea = new JTextArea();
		displayArea.setEditable(false);
		JScrollPane displayScrollPane = new JScrollPane(displayArea);
		displayScrollPane.setPreferredSize(new Dimension(575, 125));

		JPanel contentPane = new JPanel();
		contentPane.setLayout(new BorderLayout());
		contentPane.add(typingScrollPane, BorderLayout.NORTH);
		contentPane.add(displayScrollPane, BorderLayout.SOUTH);
		contentPane.add(buttonPane, BorderLayout.CENTER);
		contentPane.add(resetButton, BorderLayout.WEST);
		contentPane.add(testButton, BorderLayout.EAST);
		setContentPane(contentPane);

		setSize(new Dimension(620, 580));

		newline = System.getProperty("line.separator");
	}

	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();

		if (source == testButton) {
			playTest();
		}

		if (source == resetButton) {
			//Clear the text components.
			typingArea.setText("");
			displayArea.setText("");

			setKeyOfPiece(60);
			keyLabel.setText("No key set (default is C)");
			keyOfPieceIsSet = false; // next key press will set key again

			wordMusicShape.finishAll();
			phraseMusicShape.finishAll();
		}
		
		if (source == recordButton) {
			if (recordButton.isSelected()) {
				recordButton.setText("stop recording");
			}
			else {
				recordButton.setText("record WAV");				
			}
		}

		//Return the focus to the typing area.
		typingArea.requestFocus();
	}

	public void buildPhrase(double pitch, double keyPressDuration) {
		if (phraseStartTime == 0) {
			phraseStartTime = JMSL.realTime();
			//System.out.println("phraseStartTime: " + phraseStartTime);
		}
		phraseMusicShape.add(JMSL.realTime() - phraseStartTime, pitch - 24,
				JMSLRandom.choose(0.5) + 0.6, keyPressDuration);
	}

	public void buildWord(double pitch, double keyPressDuration) {
		wordMusicShape.add(0, pitch - 12, JMSLRandom.choose(0.1)+0.05, keyPressDuration
				* pitch / 3);
	}

	private void launchMusicShape(MusicShape myMusicShape) {
		((MusicShape) myMusicShape.clone()).launch(JMSL.now());
		myMusicShape.removeAll();
	}

	private void setKeyOfPiece(int myKeyCode) {
		String[] pitchNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#",
				"A", "A#", "B"};

		switch (myKeyCode) {
			case 65 : // A
				keyOfPiece = 9;
				break;
			case 66 : // B
				keyOfPiece = 11;
				break;
			case 67 : // C
				keyOfPiece = 0;
				break;
			case 68 : // D
				keyOfPiece = 2;
				break;
			case 69 : // E
				keyOfPiece = 4;
				break;
			case 70 : // F
				keyOfPiece = 5;
				break;
			case 71 : // G
				keyOfPiece = 7;
				break;
			default :
				keyOfPiece = myKeyCode % 12;
		}
		keyLabel.setText("In the key of " + pitchNames[(int) keyOfPiece]);
		keyOfPieceIsSet = true;
		System.out.println("Key of piece has been set to: "
				+ pitchNames[(int) keyOfPiece] + " / " + keyOfPiece);
	}

	public double keyCodeToPitch(int keyCode) {
		double returnKeyCode = 0;

		// letters in order of frequency in written English
		switch (keyCode) {
			case 69 : // E = tonic
				returnKeyCode = rootPitch;
				break;
			case 84 : // T = 5th
				returnKeyCode = rootPitch + 7;
				break;
			case 65 : // A = 3rd
				returnKeyCode = rootPitch + 4;
				break;
			case 79 : // O = 6th
				returnKeyCode = rootPitch + 9;
				break;
			case 73 : // I = 4th
				returnKeyCode = rootPitch + 5;
				break;
			case 78 : // N
				returnKeyCode = rootPitch + 11;
				break;
			case 83 : // S
				returnKeyCode = rootPitch + 2;
				break;
			case 82 : // R
				returnKeyCode = rootPitch + 12;
				break;
			case 72 : // H
			case 76 : // L
			case 68 : // D
			case 67 : // C
			case 85 : // U
			case 77 : // M
			case 70 : // F
				returnKeyCode = rootPitch + (keyCode % 12);
				break;
			default :
				returnKeyCode = (double) keyCode;
		}

		return returnKeyCode;
	}

	public void keyPressed(KeyEvent e) {
		int myKeyCode = e.getKeyCode();

		if (!keyOfPieceIsSet)
			setKeyOfPiece(myKeyCode);

		// filter out auto-repeat
		if (!isKeyPressed[myKeyCode]) {
			keyPressedTime[myKeyCode] = e.getWhen();

			// 32 = ' ' | 44 = ',' | 46 = '.' | 59 = ";"
			switch (myKeyCode) {
				case 32 : // space
				case 44 : // comma
				case 46 : // period
				case 59 : // semicolon
					// do nothing, so that these keys don't trigger sounds
					break;
				default :
					double[] dataClone = cloneDataAndSetPitch(keyCodeToPitch(myKeyCode));
					keyPressIns.on(JMSL.now(), 2.0, dataClone);
			}

			if (displayButton.isSelected())
				displayInfo(e, "KEY PRESSED: ");
			isKeyPressed[myKeyCode] = true;
		}
	}

	public void keyReleased(KeyEvent e) {
		int myKeyCode = e.getKeyCode();
		char myKeyChar = e.getKeyChar();

		switch (myKeyChar) {
			case ' ' : // " " space - 32
				//wordMusicShape.print();
				launchMusicShape(wordMusicShape);
				break;
			case '?' :
			case '!' :
			case '.' : // "." period - 46
				launchMusicShape(wordMusicShape);
			// no break so it plays the phrase
			case ',' : // "," comma - 44
			case ':' : // ":" colon
			case ';' : // ";" semi-colon - 59
				phraseMusicShape.differentiate(JMSL.realTime()
						- phraseStartTime, 0);
				phraseMusicShape.set(0, 0, 0); // set 1st duration to 0 so it
											   // starts immediately
				//phraseMusicShape.print();
				launchMusicShape(phraseMusicShape);
				phraseStartTime = 0;
				break;
			default :
				double[] dataClone = cloneDataAndSetPitch(myKeyCode);
				keyPressIns.off(JMSL.now(), 1.0, dataClone);

				// Java timestamp is in milliseconds, JSML in seconds
				double keyPressDuration = ((double) (e.getWhen() - keyPressedTime[myKeyCode])) / 1000;
				//System.out.println("keyPressDuration: " + keyPressDuration);
				double pitch = keyCodeToPitch(myKeyCode);
				buildWord(pitch, keyPressDuration);
				buildPhrase(pitch, keyPressDuration);
		}

		//System.out.println("Key press duration: " + keyPressDuration);
		if (displayButton.isSelected())
			displayInfo(e, "KEY RELEASED: ");
		isKeyPressed[myKeyCode] = false;
	}

	public void keyTyped(KeyEvent e) {
		// this is not useful to me but necessary for the KeyListener interface
	}

	public void start() {
		// JMSL
		// set advance to 5ms to make the latency predictable
		JMSL.clock.setAdvance(0.005);
		JSynMusicDevice.instance().open();
		mixer = new JMSLMixerContainer();
		mixer.start();

		keyPressIns = new SynthNoteAllPortsInstrument(8,
				KeyPressSynthNote.class.getName());
		mixer.addInstrument(keyPressIns);
		keyPressMusicShape = new MusicShape(6);
		keyPressMusicShape.setInstrument(keyPressIns);
		keyPressMusicShape.setRepeats(1);

		keyPressNameSpace = keyPressIns.getDimensionNameSpace();

		keyPressInsData = new double[keyPressNameSpace.dimension()];

		for (int i = 0; i < keyPressInsData.length; i++) {
			keyPressInsData[i] = keyPressNameSpace.getDefault(i);
			System.out.println(keyPressNameSpace.getDimensionName(i) + " "
					+ keyPressInsData[i]);
		}

		JSynInsFromClassName wordIns = new JSynInsFromClassName(32,
				SquareOscillatorSynthNote.class.getName());
		mixer.addInstrument(wordIns);
		wordMusicShape = new MusicShape(4);
		wordMusicShape.useStandardDimensionNameSpace();
		wordMusicShape.setRepeats(1);
		wordMusicShape.setInstrument(wordIns);

		JSynInsFromClassName phraseIns = new JSynInsFromClassName(24,
				com.softsynth.jsyn.circuits.Swarm.class.getName());
		mixer.addInstrument(phraseIns);
		phraseMusicShape = new MusicShape(4);
		phraseMusicShape.useStandardDimensionNameSpace();
		phraseMusicShape.setRepeats(1);
		phraseMusicShape.setInstrument(phraseIns);

		getParent().validate();
		getToolkit().sync();

		typingArea.grabFocus();
	}

	public void stop() {
		JMSL.closeMusicDevices();
	}

	/**
	 * This method clones data[] and sets the pitch from the key pressed. Cannot
	 * just send data[] to instrument.play() because JSyn Instruments use a
	 * hashtable of double[] to look up which allocated synthnote to turn off or
	 * update() later. So each double[] must be a new object .
	 */
	private double[] cloneDataAndSetPitch(double pitch) {
		double[] dataClone = new double[keyPressInsData.length];
		System.arraycopy(keyPressInsData, 0, dataClone, 0,
				keyPressInsData.length);
		dataClone[keyPressNameSpace.getDimension("pitch")] = pitch + 12;
		return dataClone;
	}

	/**
	 * We have to jump through some hoops to avoid trying to print non-printing
	 * characters such as Shift. (Not only do they not print, but if you put
	 * them in a String, the characters afterward won't show up in the text
	 * area.)
	 */
	protected void displayInfo(KeyEvent e, String s) {
		String charString, keyCodeString, modString, tmpString;

		char c = e.getKeyChar();
		int keyCode = e.getKeyCode();
		int modifiers = e.getModifiers();
		long timestamp = e.getWhen();

		if (Character.isISOControl(c)) {
			charString = "character = (an unprintable control character)";
		} else {
			charString = "character = '" + c + "'";
		}

		keyCodeString = "code = " + keyCode + " ("
				+ KeyEvent.getKeyText(keyCode) + ")";

		modString = "modifiers = " + modifiers;
		tmpString = KeyEvent.getKeyModifiersText(modifiers);
		if (tmpString.length() > 0) {
			modString += " (" + tmpString + ")";
		} else {
			modString += " (no modifiers)";
		}

		displayArea.append(timestamp + "  " + s + "    " + charString + "    "
				+ keyCodeString + "    " + modString + "  " + newline);

		//Return the focus to the typing area.
		typingArea.requestFocus();
	}

	public void playTest() {
		JSynInsFromClassName testIns = new JSynInsFromClassName(8,
				com.softsynth.jsyn.circuits.FilteredSawtoothBL.class.getName());
		mixer.addInstrument(testIns);

		// define a MusicShape with 4 dimensions
		MusicShape s = new MusicShape(4);
		s.useStandardDimensionNameSpace();
		s.setRepeats(1);

		for (int i = 0; i < 10; i++) {
			s.add(0.2, 60 + i, 0.5, 0.1);
		}
		s.setInstrument(testIns);
		testIns.setTuning(new TuningET(24, TuningET.MIDDLE_C_FREQ,
				TuningET.MIDDLE_C_PITCH, 24));
		s.launch(JMSL.now());
		//s.print();
	}

	public static void main(String[] args) {
	}
}
