/*
 * 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.File;
import java.io.IOException;

import java.util.ArrayList;

/**
 * Read and manager audio treatments
 */
public class TreatmentManager implements MixDataSource {
    private String treatment;
    private int repeatCount;
    private int sampleRate;
    private int channels;

    private boolean isPaused = false;
    private boolean isStopped = false;

    /*
     * This ArrayList contains one byte[] element per audio file.
     */
    private ArrayList<AudioSource> treatments = new ArrayList();

    private ArrayList treatmentDoneListeners = new ArrayList();

    private static String[] soundPath;

    static {
	String s = System.getProperty("com.sun.voip.server.Bridge.soundPath",
            "/com/sun/voip/server/sounds");

	String[] sp = s.split(":");

	soundPath = new String[sp.length + 1];

	for (int i = 0; i < sp.length; i++) {
	    soundPath[i] = sp[i];
	}

	/*
	 * On Windows user.dir contains a ":" such as C:\
	 */
	soundPath[sp.length] = System.getProperty("user.dir");
    }

    public TreatmentManager(String treatment, int repeatCount)
	    throws IOException {

	this(treatment, repeatCount, 8000, 1);
    }

    public TreatmentManager(String treatment, int repeatCount,
	    int sampleRate, int channels) throws IOException {

	this.treatment = treatment;
	this.repeatCount = repeatCount;
	this.sampleRate = sampleRate;
	this.channels = channels;

	if (Logger.logLevel >= Logger.LOG_MOREINFO) {
	    Logger.println("new treatment:  " + treatment
		+ " repeat " + repeatCount + " sampleRate " + sampleRate
		+ " channels " + channels);
	}

	parseTreatment(treatment);
    }

    public String getId() {
	return treatment;
    }

    public int getSampleRate() {
	AudioSource audioSource = this.audioSource;

	if (audioSource == null) {
	    audioSource = getAudioSource();
	}

	if (audioSource == null) {
	    return 0;
	}

	return audioSource.getSampleRate();
    }

    public int getChannels() {
	AudioSource audioSource = this.audioSource;

	if (audioSource == null) {
	    audioSource = getAudioSource();
	}

	if (audioSource == null) {
	    return 0;
	}

	return audioSource.getChannels();
    }

    public void pause(boolean isPaused) {
	this.isPaused = isPaused;
    }

    public boolean isPaused() {
	return isPaused;
    }


    private int[] previousContribution;
    private int[] currentContribution;

    public String getSourceId() {
	return treatment;
    }

    public boolean contributionIsInCommonMix() {
	return true;
    }

    public int[] getPreviousContribution() {
	return previousContribution;
    }

    public int[] getCurrentContribution() {
	return currentContribution;
    }

    public void saveCurrentContribution() {
	previousContribution = currentContribution;
	currentContribution = getLinearData(RtpPacket.PACKET_PERIOD);
    }

    private int treatmentIndex = 0;
    private AudioSource audioSource;
    private SampleRateConverter sampleRateConverter;

    public byte[] getLinearDataBytes(int sampleTime) {
	if (isStopped) {
	    return null;
	}

	int[] intData = getLinearData(sampleTime);

	if (intData == null) {
	    return null;
	}

	byte[] byteData = new byte[intData.length * 2];

	try {
	    AudioConversion.intsToBytes(intData, byteData, 0);
	} catch (Exception e) {
	    Logger.println("getLinearDataBytes, intData len "
		+ intData.length + " byteData len " + byteData.length);
	    e.printStackTrace();
	    return null;
	}

	return byteData;
    }

    public int[] getLinearData(int sampleTime) {
	synchronized (treatments) {
	    audioSource = getAudioSource();

	    if (audioSource == null) {
		Logger.println(
		    "Audio source is null, stopping treatment "
	  	    + treatment);
		stopTreatment();
	        return null;
	    }
	}

	int[] linearData = null;

	try {
	    linearData = audioSource.getLinearData(sampleTime);
	} catch (IOException e) {
	    Logger.println("Can't read linear data for " + treatment
		+ " " + e.getMessage());
	}

	if (linearData != null) {
	    if (sampleRateConverter != null) {
		try {
		    linearData = sampleRateConverter.resample(linearData);
		} catch (IOException e) {
		    Logger.println("Can't resample treatment! "
			+ e.getMessage());

		    return null;
		}
	    }
	    return linearData;
	}

	try {
	    audioSource.rewind();
	} catch (IOException e) {
	    Logger.println("Can't rewind treatment " + treatment
		+ " " + e.getMessage());
	}

	treatmentIndex++;

	synchronized (treatments) {
	    if (treatmentIndex >= treatments.size()) {
	        /*
	         * We're done playing all of the data, repeat if necessary
	         */
	        if (repeatCount != -1) {
		    if (repeatCount == 0) {
		        if (Logger.logLevel >= Logger.LOG_MOREINFO) {
		            Logger.println("done playing treatment "
			        + treatment);
		        }
		        stopTreatment();
		        return null;	// all done
		    }
		    repeatCount--;
	        }
	        treatmentIndex = 0;
	    }
	}

	sampleRateConverter = null;
	audioSource = getAudioSource();

	if (audioSource == null) {
	    return null;
	}

	try {
	    linearData = audioSource.getLinearData(sampleTime);

	    if (linearData != null && sampleRateConverter != null) {
		linearData = sampleRateConverter.resample(linearData);
	    }

	    return linearData;
	} catch (IOException e) {
	    Logger.println("Can't read linear data for " + treatment
		+ " " + e.getMessage());
	}
	return null;
    }

    private AudioSource getAudioSource() {
	synchronized (treatments) {
	    if (treatments.size() == 0) {
		if (Logger.logLevel >= Logger.LOG_MOREINFO) {
		    Logger.println("Treatments list is empty for " + treatment);
		}
	        return null;
	    }

	    AudioSource audioSource = treatments.get(treatmentIndex);

	    if (sampleRateConverter != null) {
	        return audioSource;
	    }

	    if (sampleRate != audioSource.getSampleRate() ||
	            channels != audioSource.getChannels()) {

	        try {
	            sampleRateConverter =
		        new SampleRateConverter("Treatment",
			    audioSource.getSampleRate(),
		            audioSource.getChannels(),
			    sampleRate, channels);
	        } catch (IOException e) {
		    Logger.println("Can't resample treatment! "
		        + e.getMessage());
		    return null;
	        }
	    }
	    return audioSource;
	}
    }

    /*
     * Register to be notified when the treatment finishes.
     */
    public void addTreatmentDoneListener(TreatmentDoneListener listener) {
	synchronized (treatmentDoneListeners) {
	    treatmentDoneListeners.add(listener);
	}
    }

    public void removeTreatmentDoneListener(TreatmentDoneListener listener) {
	synchronized (treatmentDoneListeners) {
	    treatmentDoneListeners.remove(listener);
	}
    }

    public void stopTreatment() {
	stopTreatment(true);
    }

    public void stopTreatment(boolean notify) {
	if (isStopped) {
	    return;
	}

	isStopped = true;

	for (AudioSource audioSource : treatments) {
	    audioSource.done();
	}

	synchronized (treatments) {
	    treatments.clear();
	}

	if (notify == false) {
	    return;
	}

	repeatCount = 0;

	synchronized (treatmentDoneListeners) {
	    while (treatmentDoneListeners.size() > 0) {
	        TreatmentDoneListener listener = (TreatmentDoneListener)
		    treatmentDoneListeners.remove(0);
	        listener.treatmentDoneNotification(this);
	    }
	}

	if (audioSource != null) {
	    try {
		audioSource.done();
	    } catch (Exception e) {
		Logger.println("Exception calling audioSource.done() " + e.getMessage());
	    }
	}
    }

    private void addTreatment(String path) throws IOException {
	if (Logger.logLevel >= Logger.LOG_MOREINFO) {
	    Logger.println("adding " + path);
	}

	synchronized (treatments) {
            if (path.substring(0, 1).equals(File.separator) ||
		   path.startsWith("/") ||
		   path.startsWith("http://")) {

                AudioSource as = FileAudioSource.getAudioSource(path);

		if (as == null) {
		    throw new IOException("Invalid treatment: " + path);
		}

		treatments.add(as);
		return;
	    }

	    for (int i = 0; i < soundPath.length; i++) {

                String s = soundPath[i] + File.separator + path;

                AudioSource as = FileAudioSource.getAudioSource(s);

                if (as != null) {
                    treatments.add(as);
		    return;
		}
	    }

	    throw new IOException("Invalid treatment: " + path);
	}
    }

    /*
     * The rest of this file contains methods to parse treatments and
     * add them to the treatment manager linear data vector.
     */
    private static String[] dtmf = {
        "dtmf0.au",
        "dtmf1.au",
        "dtmf2.au",
        "dtmf3.au",
        "dtmf4.au",
        "dtmf5.au",
        "dtmf6.au",
        "dtmf7.au",
        "dtmf8.au",
        "dtmf9.au",
        "dtmfpound.au",
        "dtmfstar.au"
    };

    /*
     * The treatment specifier is as follows:
     * "file:[<commas>]<path>[<commas>];file:[<commas>]<path><commas>; ...]
     * "dtmf:[<commas>]<0-9#*>[<commas>][<0-9#*>][<commas>] ...
     * "tts:<text>;<text>; ...
     * "s:<frequency>.<milliseconds>[+<frequency>.<milliseconds>...]
     * each "," is worth 100 ms of silence.
     */
    private void parseTreatment(String treatment) throws IOException {
	String treatments[] = treatment.split(";");

	if (Logger.logLevel >= Logger.LOG_MOREINFO) {
	    for (int i = 0; i < treatments.length; i++) {
	        Logger.println("treatments:  " + treatments[i]);
	    }
	}

	for (int i = 0; i < treatments.length; i++) {
	    String currentTreatment = treatments[i];

            if (currentTreatment.indexOf("dtmf:") == 0) {
		addDtmfTreatment(currentTreatment.substring(5));
	    } else if (currentTreatment.indexOf("d:") == 0) {
		addDtmfTreatment(currentTreatment.substring(2));
            } else if (currentTreatment.indexOf("tts:") == 0) {
		addTtsTreatment(currentTreatment.substring(4));
	    } else if (currentTreatment.indexOf("t:") == 0) {
		addTtsTreatment(currentTreatment.substring(2));
	    } else if (currentTreatment.indexOf("s:") == 0) {
		addSineWaveTreatment(currentTreatment.substring(2));
            } else {
		if (currentTreatment.indexOf("file:") == 0) {
		    currentTreatment = currentTreatment.substring(5);
                }
                addFileTreatment(currentTreatment);
            }
        }
    }

    /*
     * treatment looks like [<commas>]<number>[<commas>]...
     */
    private void addDtmfTreatment(String treatment) throws IOException {
	String s = treatment.replaceAll("[ \t]", "");

	for (int i = 0; i < s.length(); i++) {
	    if (s.charAt(i) == ',') {
		addSilence();
	    } else {
		addOneDtmfTreatment(s.charAt(i));
	    }
	}
    }

    private void addOneDtmfTreatment(char c) throws IOException {
        int n;

        if (c == '#') {
            n = 10;
        } else if (c == '*') {
            n = 11;
        } else {
            try {
                n = Integer.parseInt(String.valueOf(c));
            } catch (NumberFormatException e) {
                Logger.error("invalid dtmf treatment code '" + c + "'");
                return;
            }
        }

        if (n < 0 || n > 11) {
            Logger.error("invalid dtmf treatment code " + c);
	    return;
	}

        String s = dtmf[n];
        addTreatment(s);
        return;
    }

    /*
     * treatment is like <text>
     */
    private void addTtsTreatment(String treatment) throws IOException {

        int[] linearData = null;

        try {
            linearData = FreeTTSClient.textToSpeech(treatment);
        } catch (IOException e) {
            Logger.println("Can't convert text to speech '" + treatment + "' "
                + e.getMessage());
            throw new IOException("Can't convert text to speech '" + treatment
		+ "'");
        }

        treatment = "FreeTTS";

		/*
		 * TTS data is always 16000/1
		 */
		synchronized (treatments) {
			treatments.add(new LinearDataAudioSource(linearData, 16000, 1));
		}
    }

    private void addSineWaveTreatment(String treatment) throws IOException {

	String s = treatment.replaceAll("[\t]", "");

	String[] notes;

	notes = treatment.split("[\\+]");

	for (int i = 0; i < notes.length; i++) {
	    int frequency;
	    int duration = 2000;
	    float volume = 1.0F;

	    String[] param = notes[i].split("[\\.]", 3);

	    if (param.length < 1) {
		throw new IOException("missing frequency " + treatment);
	    }

	    try {
		frequency = Integer.parseInt(param[0]);
	    } catch (NumberFormatException e) {
		throw new IOException("invalid duration " + treatment);
	    }

	    if (param.length > 1) {
	        try {
		    duration = Integer.parseInt(param[1]);
	        } catch (NumberFormatException e) {
		    throw new IOException("invalid duration " + treatment);
	        }
	    }

	    if (param.length > 2) {
		try {
		    volume = Float.parseFloat(param[2]);
		} catch (NumberFormatException e) {
		    throw new IOException("invalid volume " + treatment);
		}
	    }

	    if (Logger.logLevel >= Logger.LOG_MOREINFO) {
		Logger.println("Sine treatment:  frequency "
		    + frequency + " duration " + duration + " volume " + volume);
	    }

	    synchronized (treatments) {
	        treatments.add(new SineWaveAudioSource(frequency, duration,
		    volume, sampleRate, channels));
	    }
	}
    }

    private void addFileTreatment(String treatment) throws IOException {
        /*
         * The treatment looks like this:
         *    [<commas>]<path>[<commas>]
         */
	//String s = treatment.replaceAll("[ \t]", "");
        String s = treatment;
	while (s.length() > 0) {
	    if (s.charAt(0) == ',') {
		addSilence();
		s = s.substring(1);	// skip comma
	    } else {
		String currentTreatment = s;

		int i = s.indexOf(",");	// find trailing commas

		if (i > 0) {
		    currentTreatment = s.substring(0, i);
		    addTreatment(currentTreatment);
		    s = s.substring(i);
		} else {
		    addTreatment(s);
		    break;
		}
	    }

	}

    }

    private void addSilence() throws IOException {
        addTreatment("silence.100ms.au");
    }

    public String toAbbreviatedString() {
	return treatment;
    }

}
