/*
Copyright (c) 2006, Geomatics and Cartographic Research Centre, Carleton 
University All rights reserved.

Redistribution and use in source and binary forms, with or without 
modification, are permitted provided that the following conditions are met:

 - Redistributions of source code must retain the above copyright notice, 
   this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 - Neither the name of the Geomatics and Cartographic Research Centre, 
   Carleton University nor the names of its contributors may be used to 
   endorse or promote products derived from this software without specific 
   prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.

$Id$
*/
package ca.carleton.gcrc.atlas;

import java.lang.IllegalStateException;
import java.lang.reflect.Array;

import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Receiver;
import javax.sound.midi.Transmitter;
import javax.sound.midi.InvalidMidiDataException;

import ca.carleton.gcrc.atlas.StandardMidiFilePlayer.Debug;

/**
 * Pure java MIDI software device with a single receiver and a single transmitter
 * that provides on-the-fly configurable one-pass MIDI transformation on a MIDI
 * event stream.
 * <p>
 * By one-pass, this means transformations that are possible by looking only at a
 * single message at a time or where state from prior messages applies to later 
 * messages.
 * <p>
 * Examples:
 * <ul>
 * <li> on-the-fly transposition.
 * <li> on the fly gain scaling (as a percentage of the master volume controlChanges
 *      carried within the MIDI stream).
 * </ul>
 * 
 * @author Glenn Brauen
 */  
public class OnePassMidiTransformer {
	/*
	 * constants
	 */
	private final int NUM_CHANNELS = 16;
	private final int NUM_KEYS = 128;
	
	private OnePassMidiTransformerReceiver m_receiver = null;
	private OnePassMidiTransformerTransmitter m_transmitter = null;
	
	/**
	 ** gain scale modifier - percentage of channel voice settings.
	 **/
	private final int GAIN_SCALE_DEFAULT = 100;
	private final int NO_CHANNEL_MESSAGE_GAIN_SET = 127; // if no channel gains use 127 (default = max).
	
	/* master gain scale configuration value */
	private int m_masterGainScaleModifier = GAIN_SCALE_DEFAULT; // as percent
	
	/* channel gain scale configuration values */
	private int[] m_channelGainScaleModifiers = new int[NUM_CHANNELS];

	/* flag to determine if gain scaling is configured quickly in send() */
	private boolean m_adjustGainScale = false;
	
	/* tracked gain settings carried in channel messages - used when adjusting at config time. */
	private int[] m_channelMessageGains = new int[NUM_CHANNELS];
	
	/**
	 ** note transpose - absolute number of half-steps (+ve or -ve)
	 **/
	private final int PERCUSSION_CHANNEL = 9; // 0-based (10 in General MIDI literature).
	
	/* transposition configuration */
	private int m_noteTranspose = 0;
	
	/* 
	 * when transpose first configured ... need to ensure that a note already playing gets turned off
	 * properly.  This requires me to to track the transpose value of the last note on with a non-zero
	 * velocity.
	 */
//	private byte[][] m_noteVelocities = new byte[NUM_CHANNELS][NUM_KEYS]; // indexing is [channel][key] all zero-based
	private byte[][] m_noteLastTranspose = new byte[NUM_CHANNELS][NUM_KEYS];
		
	public OnePassMidiTransformer() {
		//allocate receiver and transmitter
		m_receiver = new OnePassMidiTransformerReceiver(this);
		m_transmitter = new OnePassMidiTransformerTransmitter();
		
		// transform-specific initializations.
		for (int i=0; i<m_channelMessageGains.length; i++) {
			m_channelMessageGains[i] = NO_CHANNEL_MESSAGE_GAIN_SET; // haven't seen a channel gain setting yet.
			m_channelGainScaleModifiers[i] = GAIN_SCALE_DEFAULT; // default for each channel - no scaling.
			
			for (int j=0; j<NUM_KEYS; j++) {
//				m_noteVelocities[i][j] = 0; // note not playing - no velocity.
				m_noteLastTranspose[i][j] = 0; // no transpose initially configured.
			}
		}
	} // OnePassMidiTransformer
	
	protected final class Debug {
		  protected static final int standardLevel = 0;
		  protected static final int mediumLevel = 1;
		  protected static final int detailLevel = 2;
		  
		  protected static final boolean ON = false;
		  protected static final int LEVEL = standardLevel;
	} 
	
	protected void DebugLog(String str) {
		if (Debug.ON) System.out.println(str);
	}

	protected void DebugLog(String str, int level) {
		if (Debug.ON && level <= Debug.LEVEL) System.out.println(str);
	}

	public Receiver getReceiver() {
		return(m_receiver);
	}
	
	public Transmitter getTransmitter() {
		return(m_transmitter);
	}
	
	public void close() {
		m_transmitter.close();
		m_transmitter = null;
		m_receiver.close();
		m_receiver = null;
	} // close()
	
	public void send(MidiMessage message_, long timeStamp_) throws IllegalStateException {
		int length;
		
		// make sure we can get a receiver - or just get out...
		Receiver receiver = m_transmitter.getReceiver();
		if (null == receiver) {
			throw new IllegalStateException("no receiver connected to transmitter");
		}
		
		length = message_.getLength();
		if (3 == length) {
			
			// could be channel voice message and we want to play with those
			byte[] buf = new byte[3];
			buf = message_.getMessage();
			int statusByte = (int) (buf[0] & 0xf0);
			if (statusByte == ShortMessage.CONTROL_CHANGE) {
				if ((buf[1] & 0x7f) == 0x07) { // coarse volume(7) (ignore fine volume(39) - java synth implementation does)
					/*
					 * controlChange message structure:
					 * [byte 0]: MSN(control change), LSN(channel number)
					 * [byte 1]: controller number (0..127, high bit always clear)
					 * [byte 2]: controller setting (0..127, high bit always clear)
					 */
					
					// for future reference store the channel voice setting of the gain.
					int channelAsInt = (int) (buf[0] & 0x0f);
					m_channelMessageGains[channelAsInt] = (int) (buf[2] & 0x7f);
					
					if (m_adjustGainScale) {
						if (m_masterGainScaleModifier != GAIN_SCALE_DEFAULT ||
							m_channelGainScaleModifiers[channelAsInt] != GAIN_SCALE_DEFAULT) {
							int value = m_channelMessageGains[channelAsInt] * m_masterGainScaleModifier / 100;
							value *= m_channelGainScaleModifiers[channelAsInt] / 100;
							buf[2] = (byte) (value & 0x7f);
							ShortMessage m = new ShortMessage();
							try {
								m.setMessage((int) buf[0], (int) buf[1], (int) buf[2]);	
							} catch (InvalidMidiDataException e) {
								e.printStackTrace();
							}
							
							receiver.send(m, timeStamp_); // forward modified message
							return;	
						} else {
							// not configured to modify this channel (or master) - forward original message and get out.
							receiver.send(message_, timeStamp_);
							return;						
						}
						
					} else {
						// not configured to modify right now - forward original message and get out
						receiver.send(message_, timeStamp_);
						return;						
					}
					
				} else {
					// don't care about other controllers right now - forward original message and get out
					receiver.send(message_, timeStamp_);
					return;
				}
			} else if (statusByte == ShortMessage.NOTE_ON || statusByte == ShortMessage.NOTE_OFF) {
				// note on or note off - check for transposition
				
				// absolute scale transposition... one setting for whole composition.
				// but don't apply to percussion track (9 in zero-based channel numbers).
				
				// store all notes to keep track of velocity
				int channel = (int) (buf[0] & 0x0f);
				int key = (int) (buf[1] & 0x7f);
//				if (statusByte == ShortMessage.NOTE_ON) {
//					m_noteVelocities[channel][key] = (byte) (buf[2] & 0x7f);
//				} else {
//					m_noteVelocities[channel][key] = 0; // note off
//				}
				
				if ((m_noteTranspose != 0) && (channel != PERCUSSION_CHANNEL)) {
					/*
					 * note on/off message structure:
					 * [byte 0]: MSN(note on/off), LSN(channel number)
					 * [byte 1]: note number (0..127, high bit always clear)
					 * [byte 2]: velocity (0..127, high bit always clear)
					 * 
					 * note: note on with velocity 0 is the same as note off.
					 */
					if ((statusByte == ShortMessage.NOTE_ON) && ((buf[2] & 0x7f) > 0)) {
						m_noteLastTranspose[channel][key] = (byte) m_noteTranspose;
						buf[1] += (byte) m_noteTranspose; // TODO: what about going out of range here??
					} else {
						// if note off or no velocity, then note is off so mark last transpose as 0.
						buf[1] += m_noteLastTranspose[channel][key];
						m_noteLastTranspose[channel][key] = 0;
					}
					
					if (buf[1] < 0 || buf[1] > NUM_KEYS) {
						// out of range - generate a note off to keep timing proper.
						buf[1] = 0; // want to send note for timing reasons - put in range.
						buf[2] = 0; // set velocity to 0 (note on or off).
					}
					
					ShortMessage m = new ShortMessage();
					try {
						m.setMessage((int) buf[0], (int) buf[1], (int) buf[2]);	
					} catch (InvalidMidiDataException e) {
						e.printStackTrace();
					}
					receiver.send(m, timeStamp_); // forward modified message	
				} else {
					// not configured to modify right now or this message is for the percussion channel
					// forward original message and get out
					m_noteLastTranspose[channel][key] = 0; // note on or off

					receiver.send(message_, timeStamp_);
					return;						
				}
			} else {
				// don't care about other 3 byte messages right now - forward original
				receiver.send(message_, timeStamp_);
				return;
			}
			
		} else {
			// message doesn't fit a configured transformation - send original
			receiver.send(message_, timeStamp_);
			return;
		}
    } // send()
    
	/*
	 * Gain Scale Multiplier configuration routines.  See also send().
	 */
	private void setAdjustGainScaleFlag(int newGainScale_) {
		if (newGainScale_ != GAIN_SCALE_DEFAULT) {
			m_adjustGainScale = true;
		} else { // need to check everything.
			boolean needToAdjust = false;
			for (int i=0; i < m_channelGainScaleModifiers.length; i++) {
				if (m_channelGainScaleModifiers[i] != GAIN_SCALE_DEFAULT) { // non-default - have to alter control change messages.
					needToAdjust = true;
				}			
			}
			if (needToAdjust || (m_masterGainScaleModifier != GAIN_SCALE_DEFAULT)) {
				m_adjustGainScale = true;
			} else {
				m_adjustGainScale = false;
			}
		}
	} // setAdjustGainScaleFlag()

	public int getMasterGainScaleModifier() {
    		return m_masterGainScaleModifier;
    } // getMasterGainScaleModifier
    
    public void setMasterGainScaleModifier(int gainScale_) {
    	DebugLog("transformer: master gain set to " + gainScale_ + "%.");
    	m_masterGainScaleModifier = gainScale_;
    		setAdjustGainScaleFlag(gainScale_);
    	} // setMasterGainScaleModifier
    
    public int getLastChannelVoiceGainSetting(int channel) {
    		if (channel >= 0 && channel < m_channelMessageGains.length) { // 0-based channel number
    			return m_channelMessageGains[channel]; 
    		} else {
    			return -1;
    		}
    } // getLastChannelVoiceGainSetting

    public int getChannelGainScaleModifier(int channel_) {
		return m_channelGainScaleModifiers[channel_];
	} // getChannelGainScaleModifier()

	public void setChannelGainScaleModifier(int channel_, int gainScale_) {
    	DebugLog("transformer: channel " +  channel_ + " gain set to " + gainScale_ + "%.");
		m_channelGainScaleModifiers[channel_] = gainScale_;
		setAdjustGainScaleFlag(gainScale_);
	} // setChannelGainScaleModifier()

	/*
	 * Transpose configuration routines.  See also send().
	 */
	public void setNoteTranspose(int transpose_) {
		m_noteTranspose = transpose_;
	}

	/*
	 * D e b u g   d i s p l a y   r o u t i n e s
	 */
	
	/**
	 * Display status of Midi transformer on the java console.  Non-command-path display routine
	 * used to periodically dump the current state of the applet.
	 */
	public void displayAppletState() {
		System.out.println("   Master Gain Scale=" + m_masterGainScaleModifier);
		System.out.print("   Channel Gain Scales: ");
		for (int i=0; i < NUM_CHANNELS; i++) {
			if (i > 0) {
				System.out.print(", ");
			}
			int scale = m_channelGainScaleModifiers[i];
			System.out.print(i + "=" + scale + "%");
		}
		System.out.println();

		// TODO: add transpose to this display routine.
	} // displayAppletState()
	
} // OnePassMidiTransformer