/*
 * 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.server;

import com.sun.voip.Logger;
import com.sun.voip.Util;
import com.sun.voip.SpatialAudio;

public class SunSpatialAudio implements SpatialAudio {

    private static final double MAX_DELAY = .63;

    private static double falloff = .94;
    private static double minVolume = .7;
    private static double echoDelay = 0;   // .5 seems to be a reasonable value;
    private static double echoVolume = .35;
    private static double behindVolume = .9;
    private static int maxExp;

    private double msPerSample;

    private static double newEchoDelay = echoDelay;

    private String conferenceId;
    private String callId;
    private int sampleRate;
    private int channels;
    private int samplesPerPacket;

    private int packetLength;

    public SunSpatialAudio() {
    }

    public void initialize(String conferenceId, String callId, int sampleRate, int channels, int samplesPerPacket) {

	this.conferenceId = conferenceId;
	this.callId = callId;
	this.sampleRate = sampleRate;
	this.channels = channels;
	this.samplesPerPacket = samplesPerPacket;

	packetLength = samplesPerPacket * channels;

	msPerSample = 1000. / sampleRate;

	if (Logger.logLevel >= Logger.LOG_INFO) {
	    Logger.println(toString() + ":" + conferenceId
	        + " " + callId + "::"
	        + " sample rate " + sampleRate + ", channels " + channels
	        + ", milliseconds per sample " + msPerSample);
	}

	setMaxExp();
    }

    public static void setSpatialBehindVolume(double behindVolume) {
	Logger.println("Spatial behind volume set to "
	    + behindVolume);

        SunSpatialAudio.behindVolume = behindVolume;
    }

    public static double getSpatialBehindVolume() {
	return behindVolume;
    }

    public static void setSpatialEchoDelay(double echoDelay) {
	Logger.println("Echo delay set to " + echoDelay);
        SunSpatialAudio.newEchoDelay = echoDelay;
    }

    public static double getSpatialEchoDelay() {
        return newEchoDelay;
    }

    public static void setSpatialEchoVolume(double echoVolume) {
	Logger.println("Echo volume set to " + echoVolume);
        SunSpatialAudio.echoVolume = echoVolume;
    }

    public static double getSpatialEchoVolume() {
	return echoVolume;
    }

    public static void setSpatialFalloff(double falloff) {
	SunSpatialAudio.falloff = falloff;

	setMaxExp();
    }

    public static double getSpatialFalloff() {
	return falloff;
    }

    public static void setSpatialMinVolume(double minVolume) {
	SunSpatialAudio.minVolume = minVolume;

	if (minVolume < 0) {
	    SunSpatialAudio.minVolume = 0;
	} else if (minVolume > 1) {
	    SunSpatialAudio.minVolume = 1;
	}

	setMaxExp();
    }

    public static double getSpatialMinVolume() {
	return minVolume;
    }

    private static void setMaxExp() {
	/*
	 * Calculate the exponent by which falloff must be raised to
	 * in order to get a value near minVolume.
	 *
	 * falloff**maxExp = minVolume which is equivalent to
	 *
	 * maxExp * log(falloff) = log(minVolume) which means
	 *
	 * maxExp = log(minVolume) / log(falloff);
	 */
	if (minVolume >= falloff) {
	    maxExp = 0;
	} else {
	    maxExp = (int) ((Math.log(minVolume) / Math.log(falloff)));
	}

	if (Logger.logLevel >= Logger.LOG_INFO) {
	    Logger.println("minVolume " + minVolume + " falloff " + falloff
	        + " maxExp set to " + maxExp);
	}
    }

    int count = 0;

    /*
     * Generate spatial audio.
     */
    public int[] generateSpatialAudio(String sourceId,
	    int[] previousContribution, int[] currentContribution,
	    double[] spatialValues) {

	if (channels == 1) {
	    return currentContribution;  // no support for 1 channel conference
	}

	if (previousContribution == null && currentContribution == null) {
	    return null;	// nothing here to do
	}

	echoDelay = newEchoDelay;	// set echo delay in case it changed

	double frontBack = spatialValues[0];

	if (echoVolume == 0 || echoDelay == 0) {
	    frontBack = 0;
	}

	double leftRight = spatialValues[1];
	double volume = spatialValues[3];

	double delay = MAX_DELAY * leftRight;

	/*
	 * Calculate the number of ints for the delay.
	 * There are always 2 channels so we have to multiply by 2.
	 */
	int delayLength = (int) Math.round(delay / msPerSample) * 2;

	if (Logger.logLevel >= Logger.LOG_INFO) {
	    if ((count++ % 200) == 0) {
	        Logger.println("Delay "
	 	    + (Math.round(delay * 1000) / 1000.)
		    + " delay length " + delayLength);
	    }
	}

	int[] newContribution;

	double nonDominantChannelVolume =
	    getAttenuatedVolume(leftRight, volume);

	if (nonDominantChannelVolume < minVolume) {
	    nonDominantChannelVolume = minVolume;
	}

	if (delayLength == 0) {
	    /*
	     * Sound is in the center.
	     */
	    if (volume == 1 && frontBack >= 0) {
		/*
		 * There are no adjustments to be made.  Just return
		 * the current contribution.
		 */
		if (Logger.logLevel == -88) {
		    Util.dump("Current contribution", currentContribution, 0,
			16);
		}
		return currentContribution;
	    }

	    newContribution = new int[packetLength];

	    if (Logger.logLevel == -88) {
		Logger.println("need to make new contribution");
	    }

	    if (currentContribution != null) {
                int copyLength = Math.min(packetLength,
                                          currentContribution.length);
                System.arraycopy(currentContribution, 0, newContribution, 0,
		    copyLength);
	    }
	} else {
	    if (Logger.logLevel == -88) {
		Logger.println("do leftRight " + delayLength);
	    }

	    newContribution = doLeftRight(previousContribution,
		currentContribution, delayLength, nonDominantChannelVolume);
	}

        if (frontBack < 0) {
            if (Logger.logLevel == -88) {
		Logger.println("do frontBack " + echoDelay);
	    }

            doFrontBack(previousContribution, newContribution, frontBack,
	        delayLength, nonDominantChannelVolume);

	    volume *= getAttenuatedVolume(frontBack, behindVolume);
	}

	if (volume == 1) {
	    return newContribution;
	}

        if (Logger.logLevel == -78) {
	    Logger.println("Adjust volumes to "
		+ (Math.round(volume * 1000) / 1000.));
	}

	return adjustVolumes(newContribution, volume);
    }

    private int[] doLeftRight(int[] previousContribution,
	    int[] currentContribution, int delayLength,
	    double nonDominantChannelVolume) {

	int channelOffset;

	if (delayLength < 0) {
	    channelOffset = 1;   // delay right channel
	    delayLength = -delayLength;
	} else {
	    channelOffset = 0;   // delay left channel
	}

        int[] newContribution = new int[packetLength];

	if (currentContribution == null) {
	    /*
	     * There is no current contribution but there is a
	     * previous contribution.  Just copy the previous
    	     * contribution to our zero filled new contribution.
	     */
            int inIx = previousContribution.length - delayLength
	        + channelOffset;

	    int outIx = channelOffset;

	    for (int i = 0; i < delayLength; i += 2) {
	        newContribution[outIx] = previousContribution[inIx];
	        inIx += 2;
	        outIx += 2;
	    }

	    return newContribution;
        }

	/*
	 * There is a current contribution.  There may or may not
	 * be a previous contribution.  We must not modify the
	 * current contribution so we make a copy.
	 */
	System.arraycopy(currentContribution, 0, newContribution, 0,
	    packetLength);

	//Util.dump("new contrib", newContribution, 0, newContribution.length);

	/*
	 * Now shift the newContribution up by delayLength, then
	 * copy the previousContribution samples to the beginning
	 * of our newContribution.
	 *
	 * First copy to an intermediate buffer so we don't overwrite
	 * good data.  Otherwise, we'd have to start the copy at the
	 * end of the buffer and move downward.
	 */
	int[] c = new int[newContribution.length - delayLength];

	for (int i = channelOffset; i < c.length; i += 2) {
	    c[i] = newContribution[i];
	}

	for (int i = channelOffset; i < c.length; i += 2) {
	    newContribution[i + delayLength] = (int) (c[i] * nonDominantChannelVolume);
	}

	if (previousContribution != null) {
            int inIx = packetLength - delayLength + channelOffset;

	    int outIx = channelOffset;

	    for (int i = 0; i < delayLength; i += 2) {
	        newContribution[outIx] = (int)
		    (previousContribution[inIx] * nonDominantChannelVolume);

		inIx += 2;
		outIx += 2;
	    }
	} else {
	    //Logger.println("current but no prev");

	    for (int i = channelOffset; i < delayLength; i += 2) {
	        newContribution[i] = 0;
	    }
	}

	return newContribution;
    }

    private int count1 = 0;

    private void doFrontBack(int[] previousContribution, int[] newContribution,
	    double frontBack, int delayLength, double nonDominantChannelVolume) {

        //Util.dump("result before, p 1, c 3", newContribution, 0,
	//    newContribution.length);

        //dump(newContribution);

	int echoDelayLength = (int) Math.round(echoDelay / msPerSample);

	echoDelayLength *= Math.abs(frontBack);

	echoDelayLength *= 2;   // 2 samples for stereo

	if (echoDelayLength <= 0) {
	    return;
	}

	int channelOffset;

	if (delayLength < 0) {
	    channelOffset = 1;   // delay right channel
	    delayLength = -delayLength;
	} else {
	    channelOffset = 0;   // delay left channel
	}

        if (Logger.logLevel >= Logger.LOG_INFO) {
	    if ((count1 % 200) == 0) {
	        Logger.println("adding echo"
		    + " delayLength " + delayLength + " c off " + channelOffset
    		    + " edl " + echoDelayLength + " echoDelay " + echoDelay
		    + " msps " + (Math.round(msPerSample * 1000) / 1000.));
	   }
	}

	/*
	 * Copy newContribution
	 */
	int[] c = new int[packetLength];

	System.arraycopy(newContribution, 0, c, 0, c.length);

	int inIx = 0;
	int outIx = echoDelayLength;
	int length = c.length - echoDelayLength;

        if (Logger.logLevel >= Logger.LOG_INFO) {
            if ((count1 % 200) == 0) {
	        Logger.println("inIx " + inIx + " outIx " + outIx + " length "
	            + length);
	    }
	}

	for (int i = 0; i < length; i++) {
	    newContribution[outIx] = clip((int)
		(c[outIx] + (c[inIx] * echoVolume)));

	    inIx++;
	    outIx++;
	}

	if (previousContribution == null) {
	    count1++;
	    return;
	}

	/*
	 * Add echo from previousContribution
	 */
	if (delayLength == 0) {
	    inIx = packetLength - echoDelayLength;
	    outIx = 0;
	    length = echoDelayLength;

	    for (int i = 0; i < length; i++) {
	        newContribution[outIx] = clip((int)
		    (c[outIx] + (previousContribution[inIx] * echoVolume)));

	        inIx++;
    	        outIx++;
	    }
	    count1++;
	    return;
	}

	inIx = packetLength - delayLength - echoDelayLength
	    + channelOffset;

	outIx = channelOffset;
	length = echoDelayLength;

        if ((count1 % 200) == 0) {
	    Logger.println("inIx " + inIx + " outIx " + outIx + " length "
	        + length);
	}

	for (int i = 0; i < length; i += 2) {
	    newContribution[outIx] = clip((int)
		(c[outIx] +
		(previousContribution[inIx] * echoVolume * nonDominantChannelVolume)));

	    inIx += 2;
	    outIx += 2;
	}

	/*
	 * Add echo from the other channel
	 */
	if (channelOffset == 0) {
	    outIx = 1;
	} else {
	    outIx = 0;
	}

	inIx = outIx + packetLength - echoDelayLength;
        length = echoDelayLength - outIx;

        if ((count1 % 200) == 0) {
	    Logger.println("inIx " + inIx + " outIx " + outIx + " length "
	        + length);
	}

	int i = 0;

	for (i = 0; i < length; i += 2) {
	    newContribution[outIx] = clip((int)
		(c[outIx] + (previousContribution[inIx] * echoVolume)));

	    inIx += 2;
	    outIx += 2;
	}

	count1++;
    }

    private int[] adjustVolumes(int[] contribution, double volume) {
	/*
	 * Adjust the volume
	 */
	int[] c = new int[contribution.length];

	for (int i = 0; i < contribution.length; i++) {
	    c[i] = clip((int) (contribution[i] * volume));
	}

	return c;
    }

    private int clip(int sample) {
        if (sample > 32767) {
	    if (Logger.logLevel == -79) {
	        Logger.println("clipping " + sample + " to 32767");
	    }
            return 32767;
        }

        if (sample < -32768) {
	    if (Logger.logLevel == -79) {
	        Logger.println("clipping " + sample + " to -32768");
	    }
            return -32768;
        }

        return sample;
    }

    private double getAttenuatedVolume(double offset, double volume) {

	if (offset == 0) {
	    return 1;
	}

	int exp = (int) (Math.abs(offset) * maxExp);

	return volume * Math.pow(falloff, exp);
    }

    public String toString() {
	return "SunSpatialAudio";
    }

    public static void main(String[] args) {
	new SunSpatialAudio().test();
    }

    private void test() {
	initialize("Test", "Test", 44100, 2, 44100 / 50);

	echoDelay = .1;

        double[] spatialValues = new double[4];

        spatialValues[0] = -1;
        spatialValues[1] = .5;
        spatialValues[2] = 0;
        spatialValues[3] = 1;

	int[] p = new int[64];
        int[] c = new int[64];
        int[] result;

	fill(p, 1);
        fill(c, 3);

        //Util.dump("c before", c, 0, c.length);

        result = generateSpatialAudio("Test", p, c, spatialValues);

        Util.dump("result, p 1, c 3", result, 0, result.length);

        dump(result);

if (false) {

	p = c;
        c = new int[64];

        fill(c);

        Util.dump("c before 2", c, 0, c.length);

        result = generateSpatialAudio("Test", p, c, spatialValues);

        dump(result);

        Util.dump("p set, c set", result, 0, result.length);

        c = new int[64];

        fill(c);

        Util.dump("c before 3", c, 0, c.length);

        result = generateSpatialAudio("Test", p, c, spatialValues);

        Util.dump("c null", result, 0, result.length);

        dump(result);

        c = new int[64];

        fill(c, 4);

        Util.dump("c echo", c, 0, c.length);

        spatialValues[0] = -1;

        spatialValues[1] = 1;
        spatialValues[2] = 0;
        spatialValues[3] = 1;

        result = generateSpatialAudio("Test", p, c, spatialValues);

        Util.dump("after adding echo", result, 0, result.length);

        dump(result);

        System.out.println("====================");

        Util.dump("c echo", c, 0, c.length);

        result = generateSpatialAudio("Test", p, c, spatialValues);

        Util.dump("after adding echo", result, 0, result.length);

        dump(result);
}
    }

    private void fill(int[] buf, int v) {
        for (int i = 0; i < buf.length; i += 2) {
            buf[i] = v;
            buf[i + 1] = v;
        }
    }

    private int v = 0;

    private void fill(int[] buf) {
        for (int i = 0; i < buf.length; i += 2) {
            buf[i] = v++;
            buf[i + 1] = v++;
        }
    }

    private void dump(int[] c) {
        System.out.println("\nleft " + c.length);

        for (int i = 0; i < c.length; i += 2) {
            System.out.print(Integer.toHexString(c[i] & 0xff) + " ");
        }

        System.out.println("\n\nright " + c.length);

        for (int i = 1; i < c.length; i += 2) {
            System.out.print(Integer.toHexString(c[i] & 0xff) + " ");
        }

        System.out.println("\n");
    }

}
