/*
 * Copyright 2007 Sun Microsystems, Inc.
 *
 * This file is part of jVoiceBridge.
 *
 * jVoiceBridge is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation and distributed hereunder
 * to you.
 *
 * jVoiceBridge 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, see <http://www.gnu.org/licenses/>.
 *
 * Sun designates this particular file as subject to the "Classpath"
 * exception as provided by Sun in the License file that accompanied this
 * code.
 */

package com.sun.voip;

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

import java.text.ParseException;

import java.util.LinkedList;

/**
 * Write audio data to a file
 */
public class Recorder extends Thread {

    private BufferedOutputStream bo;
    private FileOutputStream fo;

    private static final int BUFFER_SIZE = 16 * 1024;

    private static String defaultRecordDirectory = ".";

    private String recordPath;
    private boolean recordRtp;

    private static String fileSeparator = System.getProperty("file.separator");

    private boolean done;

    public Recorder(String recordPath, String recordingType,
	    MediaInfo mediaInfo) throws IOException {

	this(null, recordPath, recordingType, mediaInfo);
    }

    public Recorder(String recordDirectory, String recordPath, String recordingType,
	    MediaInfo mediaInfo) throws IOException {

	this.recordPath = getAbsolutePath(recordDirectory, recordPath);

	if (recordingType.equalsIgnoreCase("Rtp")) {
	    recordRtp = true;
	} else if (recordingType.equalsIgnoreCase("Au") == false) {
	    throw new IOException("Invalid recording type " + recordingType);
	}

	openFile(mediaInfo);

        start();
    }

    public static String getAbsolutePath(String recordDirectory, String recordPath)
	    throws IOException {

        String osName = System.getProperty("os.name");

        if (osName.indexOf("Windows") >= 0) {
	    if (recordPath.substring(0,1).equals(fileSeparator) == true ||
	            recordPath.charAt(1) == ':') {

		return recordPath;
	    }
	} else {
	    if (recordPath.substring(0,1).equals(fileSeparator) == true) {
		return recordPath;
	    }
	}

	/*
	 * Not absolute
	 */
	if (recordDirectory == null) {
            recordDirectory = System.getProperty("com.sun.voip.server.Bridge.recordDirectory", defaultRecordDirectory);
	}

	String path = recordDirectory + fileSeparator + recordPath;

	try {
	    checkPermission(path, false);
	} catch (ParseException e) {
	    throw new IOException(e.getMessage());
	}

        return recordDirectory + fileSeparator + recordPath;
    }


    private void openFile(MediaInfo mediaInfo) throws IOException {
        File recordFile = new File(recordPath);

        try {
	    synchronized(this) {
                if (recordFile.exists()) {
		    recordFile.delete();
                }

                recordFile.createNewFile();

                fo = new FileOutputStream(recordFile);
                bo = new BufferedOutputStream(fo, BUFFER_SIZE);

                if (recordRtp == false) {
                    writeAuHeader(mediaInfo);
                } else {
                    /*
                     * Write RTP header
                     */
                    byte[] buf = new byte[16];
                    buf[0] = (byte) 0x52;  // R
                    buf[1] = (byte) 0x54;  // T
                    buf[2] = (byte) 0x50;  // P

                    bo.write(buf, 0, buf.length);
                }
	    }
        } catch (IOException e) {
            fo = null;
            bo = null;
            Logger.error("can't create buffered output stream for " + recordPath
		+ " " + e.getMessage());
            throw new IOException(
		"can't create buffered output stream for " + recordPath
		    + " " + e.getMessage());
        }

	Logger.println("Recording to " + recordFile.getAbsolutePath()
	    + " recording type is " + (recordRtp ? "RTP" : "Audio"));
    }

    public String getRecordPath() {
	return recordPath;
    }

    public static void checkPermission(String recordPath)
	    throws ParseException {

	checkPermission(recordPath, false);
    }

    public static void checkPermission(String recordPath, boolean isDirectory)
	    throws ParseException {

	if (isDirectory) {
	    File file = new File(recordPath);

	    if (file.exists() == false) {
	        throw new ParseException(
		    "Non-existent directory:  " + recordPath, 0);
	    }

	    if (file.isDirectory() == false) {
	        throw new ParseException("Not a directory:  " + recordPath, 0);
	    }

	    if (file.canWrite() == false) {
	        throw new ParseException("Permission denied.  Can't write "
		    + recordPath, 0);
	    }

	    return;
	}

	File file = new File(recordPath);

	try {
	    if (file.exists()) {
		if (file.isDirectory()) {
		    throw new ParseException("Not a regular file:  "
			+ recordPath + ".", 0);
		}
	    }

	    /*
	     * Try to create a file in the directory
	     */
	    String directory = defaultRecordDirectory;

	    int i = recordPath.lastIndexOf(fileSeparator);

	    if (i > 0) {
		directory = recordPath.substring(0, i);
	    }

	    file = File.createTempFile("Record", "tmp", new File(directory));

	    file.delete();
	} catch (IOException e) {
	    throw new ParseException("Unable to create file " + recordPath
		+ ".  " + e.getMessage(), 0);
        }
    }

    private byte[] auHeader;

    private void writeAuHeader(MediaInfo mediaInfo) throws IOException {
        /*
         * write a .au header to the file
         *
         * magic:      4 bytes    ".snd"
         * hdrsize:    4 bytes
         * datasize:   4 bytes    0
         * encoding:   4 bytes     1 for ulaw, 3 for linear
         * sampleRate: 4 bytes
         * channels:   4 bytes
         */
        auHeader = new byte[24];

        auHeader[0] = '.';
        auHeader[1] = 's';
        auHeader[2] = 'n';
        auHeader[3] = 'd';

        auHeader[7] = 24;

        int encoding = mediaInfo.getEncoding();

        if (encoding == RtpPacket.PCMU_ENCODING) {
            auHeader[15] = 1;
        } else {
            auHeader[15] = 3;
        }

        int sampleRate = mediaInfo.getSampleRate();

        auHeader[16] = (byte)((sampleRate >> 24) & 0xff);
        auHeader[17] = (byte)((sampleRate >> 16) & 0xff);
        auHeader[18] = (byte)((sampleRate >> 8) & 0xff);
        auHeader[19] = (byte)(sampleRate & 0xff);

        int channels = mediaInfo.getChannels();

        auHeader[20] = (byte)((channels >> 24) & 0xff);
        auHeader[21] = (byte)((channels >> 16) & 0xff);
        auHeader[22] = (byte)((channels >> 8) & 0xff);
        auHeader[23] = (byte)(channels & 0xff);

        bo.write(auHeader, 0, auHeader.length);
    }

    public void done() {
        if (done) {
            return;
        }

        done = true;

        synchronized(dataToWrite) {
            dataToWrite.notifyAll();
        }
    }

    /*
     * Write data to a file
     */
    private LinkedList dataToWrite = new LinkedList();

    class DataToWrite {
        public byte[] data;
	public int offset;
        public int length;

        public DataToWrite(byte[] data, int offset, int length) {
	    /*
	     * We have to copy the data, otherwise caller could
	     * overwrite it.
	     */
            this.data = new byte[length];
	    this.offset = offset;
            this.length = length;

	    System.arraycopy(data, offset, this.data, 0, length);
        }
    }

    private long lastWriteTime;

    public void writePacket(byte[] data, int offset, int dataLength)
	    throws IOException {

	if (recordRtp) {
	    byte[] buf = new byte[dataLength + 4];

            int timeChange;

	    long now = System.currentTimeMillis();

            if (lastWriteTime == 0) {
                timeChange = 0;
            } else {
                timeChange = (int) (now - lastWriteTime);
            }

	    lastWriteTime = now;

            buf[0] = (byte) ((buf.length >> 8) & 0xff);
            buf[1] = (byte) (buf.length & 0xff);
            buf[2] = (byte) ((timeChange >> 8) & 0xff);
            buf[3] = (byte) (timeChange & 0xff);

	    System.arraycopy(data, offset, buf, 4, dataLength);
	    write(buf, 0, buf.length);
	} else {
	    write(data, offset, dataLength);
	}
    }

    public void write(int[] data, int offset, int length) throws IOException {
	byte[] byteData = new byte[length * 2];

	for (int i = 0; i < length; i++) {
	    byteData[(2 * i)] = (byte) ((data[i + offset] >> 8) & 0xff);
	    byteData[(2 * i) + 1] = (byte) (data[i + offset] & 0xff);
	}

        write(byteData, 0, byteData.length);
    }

    public void write(byte[] data, int offset, int length) throws IOException {
        if (done) {
            return;
        }

        synchronized(dataToWrite) {
            dataToWrite.add(new DataToWrite(data, offset, length));
            dataToWrite.notifyAll();
        }
    }

    private int dataSize;

    public void run() {
	long lastWriteTime = 0;

        while (true) {
            synchronized(dataToWrite) {
		if (done) {
		    break;
		}

                if (dataToWrite.size() == 0) {
                    try {
                        dataToWrite.wait();
                    } catch (InterruptedException e) {
                    }

		    if (done) {
			break;
		    }
		}

	        while (dataToWrite.size() > 0) {
	    	    writeData((DataToWrite) dataToWrite.remove(0));
		}

                continue;
	    }
	}

	/*
	 * Flush out remaining data
	 */
	synchronized (dataToWrite) {
	    while (dataToWrite.size() > 0) {
	        writeData((DataToWrite) dataToWrite.remove(0));
	    }
	}

	writeDataSize();
    }

    private void writeData(DataToWrite d) {
        try {
	    synchronized(this) {
                bo.write(d.data, 0, d.length);
		dataSize += d.length;
            }
        } catch (IOException e) {
            Logger.println("Can't record to " + recordPath);
	    done();
        }
    }

    private void writeDataSize() {
        try {
	    synchronized(this) {
		if (bo != null) {
		    bo.flush();
                    bo.close();
		    fo.flush();
                    fo.close();
		}

		if (auHeader != null) {
		    /*
		     * Now write the data size in the auHeader
		     */
		    auHeader[8]  = (byte) ((dataSize >> 24) & 0xff);
		    auHeader[9]  = (byte) ((dataSize >> 16) & 0xff);
		    auHeader[10] = (byte) ((dataSize >> 8) & 0xff);
		    auHeader[11] = (byte) (dataSize & 0xff);

		    try {
                        RandomAccessFile raf = new RandomAccessFile(
			    recordPath, "rw");

		        raf.write(auHeader);
			raf.close();
		    } catch (FileNotFoundException e) {
			Logger.println(
			    "Unable to write data size to recording "
			    + recordPath + " " + e.getMessage());
		    }
		}
	    }
        } catch (IOException e) {
	    Logger.println("Exception closing recording " + recordPath
		+ " " + e.getMessage());
        }
    }

    public static void setDefaultRecordingDirectory(
	    String defaultRecordDirectory) throws ParseException {

	checkPermission(defaultRecordDirectory, true);

        Recorder.defaultRecordDirectory = defaultRecordDirectory;

        Logger.println("Default recording directory set to "
                + defaultRecordDirectory);
    }

    public static String getRecordingDirectory() {
        return defaultRecordDirectory;
    }

}
