/**
 * Author: Kunal Kandekar
 */

import java.awt.*;
import java.net.Socket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.*;
import java.nio.channels.*;

import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.File;

//audio stuff
import javax.sound.sampled.DataLine;
import javax.sound.sampled.TargetDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineEvent;

public class AudioSource implements Runnable {
	private TargetDataLine			tgtLine;
	private AudioInputStream		audioInputStream;
	private InputStream				dataInputStream;
	private OutputStream			dataOutputStream;
	private boolean					rec;
	
	private int nSamples;
	private byte [] buffer;

	private static final int	EXTERNAL_BUFFER_SIZE = 128000;


	private AudioFormat	audioFormat;
	private boolean started = false;

	private float rate;
	private int bits;
	private int channels;
	private int framesize;
	private float framerate;
	private boolean bigendian;
	private File file;

	private boolean doS2F = true;
	private float [] fdata;
	private float [] fbuffer;
	private short [] sbuffer;
	private boolean  isDone = false;
	private boolean  throttle = false;

    public AudioSource(int samples) {
    	nSamples = samples;
    	buffer = new byte[nSamples]; 
		rate = 44100.0F;
		bits = 16;
		channels = 2;
		framesize = 4;
		framerate = rate;
		bigendian = false;
		init();
		openTargetLine();
	}

    public AudioSource(int samples, File f) {
    	nSamples = samples;
    	buffer = new byte[nSamples]; 
    	file = f;
		init(file);
		/*
		int rightSize = samples*(bits/8)*channels;
		System.out.println("resize? "+size+" ->"+samples+"*"+bits+"*"+channels+" = "+rightSize);
		if(size < rightSize) {
			resize(rightSize);
		}
		*/
		//openTargetLine();
	}

    public AudioSource(int samples, float r, int b) {
    	this(samples, r, b, null, 2);
	}
	
    public AudioSource(int samples, float r, int b, String line, int c) {
    	nSamples = samples;
    	buffer = new byte[nSamples]; 
		rate = r;
		bits = b;
		/* For simplicity, the audio data format used for recording
		   is hardcoded here. We use PCM 44.1 kHz, 16 bit signed,
		   stereo.
		*/
		channels = c;
		framesize = 4;
		framerate = rate;
		bigendian = false;
		init();
		openTargetLine(line);
	}

    public AudioSource(int samples, AudioFormat audio) {
    	nSamples = samples;
    	buffer = new byte[nSamples]; 
		audioFormat = audio;
		initAudioFormat();
		openTargetLine();
	}

	public AudioFormat getAudioFormat() {
		return audioFormat;
	}

	public void setConvertShortToFloat(boolean b) {
		doS2F = b;
		if(doS2F) {
			fdata = new float[nSamples];
		}
	}
	
	//System.out.println("read "+nbytes+" samples = "+n+ " bytes");
	static String bksp = "\r";

	public static int convertPCM2Float(boolean b, byte [] pcmd, float [] fd, int n) {
		float f = 0;
		int s = 0;
		//System.out.println("p2f n="+n+" pcmd="+pcmd.length+" fd="+fd.length);
		if(b) {
			for(int i = 0; i < n; i++) {
				fd[i] = (float)((pcmd[i*2] << 8) | (pcmd[i*2 + 1] & 0xFF)) / 32768.0F;
			}
		}
		else {
			for(int i = 0; i < n; i++) {
				fd[i] = (float)((pcmd[i*2] & 0xFF) | (pcmd[i*2 + 1] << 8)) / 32768.0F;
			}
		}
		return n;
		/*
			case CT_16SB:
				output[sample]=
				    ((float) ((input[inputOffset]<<8)
				              | (input[inputOffset+1] & 0xFF)))*invTwoPower15;
				break;
			case CT_16SL:
				output[sample]=
				    ((float) ((input[inputOffset+1]<<8)
				              | (input[inputOffset] & 0xFF)))*invTwoPower15;
		*/
	}
	

    public int read(int nbytes, byte [] rawdata, float [] fdata) {
		int n = 0;
		int left = 0;
		int read = 0;
		nbytes = nSamples*2;
		try {
			left = nbytes;
			if(isDone) {
				return -1;
			}
			//if(audioInputStream != null) {
				if(nbytes > audioInputStream.available()) {
					nbytes = audioInputStream.available();
				}
				while (n < nbytes) {
					//System.out.println("Read="+read+" total="+nbytes+" n="+n+" left="+left+" dat="+rawdata.length);
					read = audioInputStream.read(rawdata, n, left);					
					if(read <= 0) {
						//error... stream closed
						isDone = true;
						break;
					}
					n += read;
					left -= read;
				}
			//}
			/*else {
				if(nbytes > tgtLine.available()) {
					nbytes = tgtLine.available();
				}
				while (n < nbytes) {
					read = tgtLine.read(output, n, left);
					//System.out.println(this+": work "+nbytes+":"+n+":"+left+"="+read);
					if(read <= 0) {
						//error... stream closed
						isDone = true;
						break;
					}
					n += read;
					left -= read;
				}
			}*/
		}
		catch (IOException e) {
			e.printStackTrace();
			return -1;
		}

		if(doS2F) {
			//convert to float array
			n = n/2;
			n = convertPCM2Float(bigendian, rawdata, fdata, n);
			//avg
//			float total = 0;
//			int n0 = 0;
//			for(int i = 0; i < n; i++) {
//			    total += fdata[i];
//			    if(fdata[i] == 0) n0++;
//			}
//			System.out.println("Stats total="+total+" n="+n+" avg="+(total/n)+ " nzeros="+n0);
			//n = Utils.toByteArray(fdata, output, n);
		}
		//System.out.print(bksp+"Wrote "+ n + " bytes rr="+runrate+" diff="+diff+" ms   ");// s/r="+nSamples+"/"+rate+"="+((float)nSamples / rate));
		return n;
	}

	public void init() {
		audioFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
			rate, bits, channels, framesize, framerate, bigendian);
		initAudioFormat();
	}

	private void initAudioFormat()	{
		if(audioFormat != null) {
			rate = audioFormat.getSampleRate();
			bits = audioFormat.getSampleSizeInBits();
			channels = audioFormat.getChannels();
			framesize = audioFormat.getFrameSize();
			framerate = audioFormat.getFrameRate();
			bigendian = audioFormat.isBigEndian();
		}
	}


	public void init(File file)	{
		try {
			audioFormat = AudioSystem.getAudioFileFormat(file).getFormat();
			initAudioFormat();
			audioInputStream = AudioSystem.getAudioInputStream(file);
		}
		catch(UnsupportedAudioFileException uafEx) {
		}
		catch(IOException ioEx) {
		}
		//play(AudioSystem.getAudioInputStream(file), audioFormat);
		//System.out.println("file format = " + audioFormat);
	}


	public void cleanup() {
		if(started) {
			tgtLine.stop();
			tgtLine.close();
			started = false;
		}
	}

    public void openTargetLine() {
        openTargetLine(null);
    }
    
	public void openTargetLine(String line) {
		DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat);
		try {
			tgtLine = (TargetDataLine) AudioSystem.getLine(info);
			tgtLine.open(audioFormat, nSamples*audioFormat.getFrameSize());
			//listener
			tgtLine.addLineListener(new LineListener() {
				public void update(LineEvent evt) {
					System.out.println("Line event:" +evt);
					System.out.println("tgtLine:" +tgtLine);
				}
			});

			audioInputStream = new AudioInputStream(tgtLine);
			if(!started) {
				tgtLine.start();
				started = true;
			}

		}
		catch (LineUnavailableException e) {
			//out("unable to get a recording line");
			e.printStackTrace();
			//System.exit(1);
		}
	}
	
	String host = "localhost";
	int port = 6555;
	boolean running = false;
	boolean done = false;
	
	public void run() {
		running = true;
		System.out.println("audio format = " + audioFormat);
		try{ 
			//first try connecting			
			Socket socket = new Socket();
	    	socket.setReuseAddress(true);
			socket.connect(new InetSocketAddress(host, port), 5000);
			System.out.println("connected to "+socket.getRemoteSocketAddress());
	    	
	    	OutputStream ops = socket.getOutputStream();
	    		    	
			float [] audioIn = new float[nSamples]; //plotSignal.getBuffer();
			//server waits for some string before it kicks off
			//send("");
			byte [] audioInRaw = new byte[nSamples * 2];	//2 bytes per sample
			ByteBuffer byteBuf = ByteBuffer.allocate(4 * audioIn.length);
            FloatBuffer floatBuf = byteBuf.asFloatBuffer();

			while(!done) {
				try {
                    int n = read(audioInRaw.length, audioInRaw, audioIn);
                    floatBuf.position(0);
                    floatBuf.put(audioIn, 0, n);
                    byte [] bytes = byteBuf.array();
                    ops.write(bytes, 0, bytes.length);
				}
				catch(Exception ex) {
					ex.printStackTrace();
					break;
				}
			}
			ops.flush();
			ops.close();
			socket.close();
		}
		catch(Exception ex) {
			ex.printStackTrace();
		}
		running = false;
		cleanup();
		//log("Stopping run");
	}
	
	public void start(String h, int p) {
        this.host = h;
        this.port = p;
		if (!running) {
    		done = false;
    		Thread thread = new Thread(this);
    		thread.start();
		}
		else {
			//log("Still running");
		}		
	}
	
	public void stop() {
	   running = false;
	   done = false;
	}

    //To build: javac AudioSource.java
    //To run: java AudioSource localhost 6666 44100 input 16 2
    public static void main(String [] args) {
    	int samplerate = 44100;
    	int nsamples = samplerate * 1;
    	int bps = 16;
    	int channels = 1;
    	String line = null;
    	try {
	    	if (args.length < 1) {
	    		System.out.println("Usage: java AudioSource <host> <port> [samplerate] [line] [bits/sample] [channels] [samples]");
	    		return;
	    	}
	    	if (args.length > 2) {
                samplerate = Integer.decode(args[2]);
    	    	if (args.length > 3) {
    	    	    line = args[3];
        	    	if (args.length > 4) {
                        bps = Integer.decode(args[4]);
            	    	if (args.length > 5) {
                            channels = Integer.decode(args[5]);
                	    	if (args.length > 6) {
                                nsamples = Integer.decode(args[6]);
                	    	}
            	    	}
        	    	}
    	    	}
	    	}
    		AudioSource audioSource = new AudioSource(nsamples, samplerate, bps, line, channels);
            audioSource.start(args[0], Integer.decode(args[1]));

    	}
    	catch(Exception ex) {
    		ex.printStackTrace();
    	}
	}
}
