package kr.gui;

import java.io.IOException;
import java.util.List;

import javax.management.Notification;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat.Encoding;

import kr.util.Util;

import kr.AudioModel;
import kr.TimeKeeper;
import kr.util.Listener;
import kr.util.ListenerManager;
import kr.util.NotificationManager;
import kr.util.audio.AudioClip;
import kr.util.audio.MidiAudioClip;

/**
 * Plays notes in as close to real time as possible. 
 * <p>
 * <b>WARNING:</b> When changing the aspects of the audio clip, the write buffer size or the current position while playing
 * the changes should be synchronized against this object. However, the method getWriteBufferSize() can be overridden, in which case,
 * no synchronization is needed.
 */
public class AudioPlayController implements Runnable
{	
	private SourceDataLine line;
	private AudioClip audioOut;
	private byte[] data = new byte[MAX_WRITE_BUFFER_SIZE];
	private int writeBufferSize = MAX_WRITE_BUFFER_SIZE;
	private int curPos = 0;
	private int notifyPos;
	private double playbackRate = 1;
	private boolean stopped = true;
	
	private long posAdjust;
	
	public AudioPlayController()
	{
	}

	public void run() {
		try {
			synchronized (this)
			{
				stopped = false;
			}
			
			AudioFormat targetFormat = new AudioFormat(Encoding.PCM_SIGNED, 
					(float)(audioOut.getRate()), audioOut.getSampleSizeInBits(), audioOut.getNumChannels(), 
					audioOut.getNumChannels() * audioOut.getSampleSizeInBits() / 8, 
					(float)(audioOut.getRate()), false);
			rawplay(targetFormat, audioOut);
		} catch (IOException e) {
			e.printStackTrace();
			NotificationManager.inst().error("Internal error, save your work and exit asap!");
		} catch (LineUnavailableException e) {
			e.printStackTrace();
			NotificationManager.inst().error("Internal error, save your work and exit asap!");
		}
	}
	
	public void stop()
	{
		if(line == null)
			return;
		
		//simple close the line, and the thread should stop automatically
		line.stop();
		line.drain();
		line.close();
		
		synchronized(this)
		{
			while(!stopped )
				try {
					wait();
				} catch (InterruptedException e) {
					NotificationManager.inst().error("Internal error, save your work and exit asap!");
					e.printStackTrace();
				}
		}
	}
	
	protected void setPlaybackRate(double playbackRate) 
	{
		if(this.playbackRate == playbackRate)
			return;
		
//		stop();
		this.playbackRate = playbackRate;
		//new Thread(this).start();
	}
	
	protected long getMicrosecondPosition() {
		return (long) (line == null ? 0 : line.getMicrosecondPosition() * playbackRate);
		//return (long) (line == null ? 0 : ((double)(curPos + posAdjust) / playbackRate /audioOut.getRate() / audioOut.getNumChannels() / audioOut.getSampleSizeInBits() * 8000000));
	}

	//this is the optimal amount of data to have in the outgoing audio buf before we write another chunk
	//to it. Too little and the sound will cut out. Too much, and the audio track can't stop quickly
	private static final int OPTIMAL_DATA_IN_AUDIO_BUF = 8192;
	public static final int MAX_WRITE_BUFFER_SIZE = 8192;	

	private void rawplay(AudioFormat targetFormat, AudioClip audioClip)
			throws IOException, LineUnavailableException {
		//byte lastByte=0;
		
		
		line = getLine(targetFormat);
		if (line != null) {
			// Start
			line.start();
			
			int length = audioClip.length();
			
			int count = 0;
			
			//sleep so that the buffer should be about used up by the time we start again
			//(bps * sl - v0 + 2 * v1 - v2)/bps
			
			double bytesPerMicro = audioClip.getBytesPerSecond()  / 1000000.;
			
			long sleepAmount = 1000;//Math.round(1000000 * WRITE_BUFFER_SIZE / audioClip.getRate() /audioClip.getNumChannels() * .1);
			int lastDataInBuf = -1;
			
			int lastCurPos = -1;
			
			while(curPos < length)
			{
				int bytesToWrite;
				boolean notifyFlag = false;
				
				//if we are playing forever and the current pos is past the point where the
				//int value will wrap around, set it to zero
				if(length == Integer.MAX_VALUE && length - curPos < MAX_WRITE_BUFFER_SIZE)
					curPos = 0;
				
				synchronized (this)
				{
					bytesToWrite = Math.min(MAX_WRITE_BUFFER_SIZE, (int) ((length - curPos)/playbackRate));
					
					
					int dataInBuf = line.getBufferSize() - line.available();
										
					if(lastDataInBuf != -1)
					{
						sleepAmount = Math.round((bytesPerMicro * sleepAmount - lastDataInBuf + 2 * dataInBuf - OPTIMAL_DATA_IN_AUDIO_BUF)/
								bytesPerMicro);
						if(sleepAmount < 0)
						{
							sleepAmount = 0;
						}
					}
					lastDataInBuf = dataInBuf;
					
//					if(count++ % 10 == 0)
//					{
//						System.out.println("avail is "+line.available()+", buffer size is "+line.getBufferSize()+"sleep amount is "+sleepAmount+
//								"data in buf is "+dataInBuf
////								+" freq is "+((MidiAudioClip)audioClip).getFreqRatio()*((MidiAudioClip)audioClip).baseFreq+
//								//" note is "+((RealTimeNotePlayController)this).getNote()
//									);
//					}
					
					int bytesRead;
					
					if(playbackRate == 1)
					{
						audioClip.copyTo(curPos,bytesToWrite, data, 0);
						bytesRead = bytesToWrite;
					}
					else
					{
						bytesRead = convertToPlaybackRate(audioClip, curPos, bytesToWrite, data, 0);
					}
					
					if(curPos < notifyPos && curPos + bytesRead >= notifyPos)
					{
						notifyFlag = true;
					}

					curPos += bytesRead;
				} //synchronized
				
				if(notifyFlag) 
					notifyCrossedPos(); 
				/*for(int i = 0; i < bytesToWrite-1; i++)
				{
					data[i] = audioClip.byteAt(curPos+i);
					if(Math.abs(data[i] - lastByte) > 50)
						System.out.println(String.format("ai caramba curPos is %d, i is %d, data[i] is %d lastByte is %d",curPos, i,data[i],lastByte));
					lastByte = data[i];
					//System.out.println(String.format("%5d: %3d", curPos, data[i]));
				}*/
				
				int bytesWritten = line.write(data, 0, bytesToWrite);
				//System.out.println("zzz"+Util.toHex(data, 0, Math.min(bytesToWrite,1024)));
				
				notifyBufferWrote(bytesWritten); 
				
				
				//if we did not write what we asked for, this means the line has been stopped
				if(bytesWritten != bytesToWrite)
				{
					synchronized (this)
					{
						stopped = true;
						notifyAll();
					}

					return; //so exit
				}
				
				try {
					if(sleepAmount > 0)
						Thread.sleep(sleepAmount / 1000,(int)( (sleepAmount % 1000) * 1000));
				} catch (Exception e) {
					NotificationManager.inst().error("Internal error, save your work and exit asap!");
					e.printStackTrace();
				}
					
			}
			
			// we wrote everything, so notify the model that we're done playing
			line.drain();
			line.stop();
			line.close();
			
			synchronized (this)
			{
				stopped = true;
				notifyAll();
			}

			notifyStopped();
		}
	}
	
	private byte [] tempBuf;

	/**
	 * Changes the playback rate of the given audio clip.
	 * TODO: maybe make this an audio clip itself.
	 * @return bytes read
	 */
	private int convertToPlaybackRate(AudioClip audioClip, int offset, int bytesToWrite, byte[] data, int destOffset) {
		
		//NOTE: this is not the most accurate way to do this, but since it's for internal half and three quarter
		//speed playback, it's fine for this purpose.
		
		int acLength = (int) Math.ceil(bytesToWrite * playbackRate);
		
		if(tempBuf == null || tempBuf.length < acLength)
			tempBuf = new byte[acLength+2];
		
		audioClip.copyTo(offset, acLength+2, tempBuf, 0);
		
		float tempOffset = 0;
		for(int i = destOffset; i < destOffset + bytesToWrite; i+=2)
		{
			if((int)tempOffset == tempOffset)
			{
				data[i] = tempBuf[(int) tempOffset * 2];
				data[i+1] = tempBuf[(int) tempOffset * 2+1];
			}
			else
			{
				byte v1 = tempBuf[(int) tempOffset*2];
				byte v2 = tempBuf[(int) tempOffset*2+1];
				byte v3 = tempBuf[(int) tempOffset*2+2];
				byte v4 = tempBuf[(int) tempOffset*2+3];
				
				float ratio = (float)(tempOffset - (int)tempOffset);
				data[i] = (byte) Math.round(v1 * (1-ratio) + v3 * ratio);
				data[i+1] = (byte) Math.round(v2 * (1-ratio) + v4 * ratio);
				//data[i] = (byte) (Math.round(v2 - 1./6. * (2*v1 + 3*v2 - 6 * v3 + v4) * ratio
				//- .5 * (-v1 + 2*v2 - v3) * ratio * ratio -
				//1./6. * (v1 - 3* v2 + 3*v3 - v4) * ratio * ratio * ratio));
				if(data[i] > 100)
				{
					float x = data[i];
					x = x +1;
				}	
				//System.out.println(data[i]);
			}  
			
			tempOffset+=playbackRate;
		}
		
		return acLength;
	}

	/**
	 * Will call notifyCrossedPos when we cross it while playing.
	 */
	protected void setNotifyPos(int notifyPos) {
		this.notifyPos = notifyPos; 
	}
	
	/**
	 * Is called when a buffer is written. May be overridden
	 */
	protected void notifyCrossedPos()
	{
	}

	/**
	 * Is called when a buffer is written. May be overridden
	 */
	protected void notifyBufferWrote(int size)
	{
	}

	
	/**
	 * Is called when the playing thread stops. May be overridden.
	 */
	protected void notifyStopped()
	{
	}

	private static SourceDataLine getLine(AudioFormat audioFormat)
			throws LineUnavailableException {
		SourceDataLine res = null;
		DataLine.Info info = new DataLine.Info(SourceDataLine.class,
				audioFormat);
		res = (SourceDataLine) AudioSystem.getLine(info);
		res.open(audioFormat);
		return res;
	}

	public AudioClip getAudioOut() {
		return audioOut;
	}

	public void setAudioOut(AudioClip audioOut) {
		this.audioOut = audioOut;
	}

	public int getCurPos() {
		return curPos;
	}

	public void setCurPos(int curPos) {
		posAdjust += this.curPos - curPos;
		this.curPos = curPos;
	}

	/**
	 * This may be overridden to re-calculate the write buffer size
	 * on every write. Do not return more than MAX_WRITE_BUFFER_SIZE
	 */
	public int getWriteBufferSize() {
		return writeBufferSize;
	}

	public void setWriteBufferSize(int writeBufferSize) {
		this.writeBufferSize = writeBufferSize;
	}

	public double getPlaybackRate() {
		return playbackRate;
	}

}
