/*
 * 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 java.lang.reflect.Constructor;

import java.util.ArrayList;

import com.sun.voip.AudioConversion;
import com.sun.voip.Logger;
import com.sun.voip.SpatialAudio;
import com.sun.voip.MixDataSource;
import com.sun.voip.RtpPacket;

public class MixManager {
    private static final int MAX_VOLUME = 16;

    private ArrayList mixDescriptors = new ArrayList();

    private ConferenceMember member;
    private int conferenceSamplesPerPacket;
    private int channels;

    private boolean useFastMix = false;

    private SpatialAudio sa;

    public MixManager(ConferenceMember member,
        int conferenceSamplesPerPacket, int channels) {

    this.member = member;
    this.conferenceSamplesPerPacket = conferenceSamplesPerPacket;
    this.channels = channels;

    /*
     * Calculate the sample rate.
     * Each packet has 20ms of data (50 packets per second).
     */
    int sampleRate = 50 * (conferenceSamplesPerPacket / channels);

    sa = getSpatialAudio();

    if (Logger.logLevel >= Logger.LOG_INFO) {
        Logger.println("Using spatial audio module:  " + sa);
    }

    sa.initialize(member.getConferenceManager().getId(),
        member.getCallParticipant().getCallId(), sampleRate,
        channels, conferenceSamplesPerPacket / channels);
    }

    private SpatialAudio getSpatialAudio() {
        String s = System.getProperty("com.sun.voip.server.SPATIAL_AUDIO");

        if (s != null) {
            try {
                Class micClass = Class.forName(s);
                Class[] params = new Class[] { };

                Constructor constructor = micClass.getConstructor(params);

                if (constructor != null) {
                    Object[] args = new Object[] { };

                    return (SpatialAudio) constructor.newInstance(args);
                }

                Logger.println("constructor not found for: " + s);
            } catch (Exception e) {
                Logger.println("Error loading '" + s + "': "
                    + e.getMessage());
            }
    }

        return new SunSpatialAudio();
    }

    public void addMix(MixDescriptor mixDescriptor) {
    MixDataSource mixDataSource = mixDescriptor.getMixDataSource();

    if (mixDataSource instanceof WhisperGroup) {
        WhisperGroup wg = (WhisperGroup) mixDataSource;

        if (wg.hasCommonMix() == false) {
        if (Logger.logLevel >= Logger.LOG_INFO) {
            Logger.println("No common mix, not adding " + wg);
        }
        return;
        }
    }

    MixDescriptor md = findMixDescriptor(mixDataSource);

    if (md != null) {
        removeMix(md);
    }

    mixDescriptors.add(mixDescriptor);
    setUseFastMix();
    }

    public void addMix(MixDataSource mixDataSource, double attenuation) {
    MixDescriptor mixDescriptor = findMixDescriptor(mixDataSource);

    if (attenuation == 0) {
        if (mixDescriptor != null) {
            if (Logger.logLevel >= Logger.LOG_MOREINFO) {
            Logger.println("Call " + member
                + " Remove mix, volume 0 " + " mixDataSource "
            + mixDataSource);
        }
        removeMix(mixDescriptor);
        } else {
            if (Logger.logLevel >= Logger.LOG_MOREINFO) {
            Logger.println("Call " + member
            + " no need to add " + mixDataSource + " volume 0");
        }
        }

        setUseFastMix();
        return;
    }

    if (mixDescriptor == null) {
        mixDescriptor = new MixDescriptor(mixDataSource, attenuation);
        mixDescriptors.add(mixDescriptor);

        if (Logger.logLevel >= Logger.LOG_MOREINFO) {
        Logger.println("created new mix for " + mixDataSource
            + " " + attenuation);
        }

        setUseFastMix();
        return;
    }

    mixDescriptor.setAttenuation(attenuation);
    setUseFastMix();
    }

    public void removeMix(MixDataSource mixDataSource) {
    MixDescriptor mixDescriptor = findMixDescriptor(mixDataSource);

    if (mixDescriptor == null) {
        if (Logger.logLevel >= Logger.LOG_MOREINFO) {
            Logger.println("Didn't find MixDescriptor for "
            + mixDataSource);
        }
        return;
    }

    removeMix(mixDescriptor);
    }

    public void removeMix(MixDescriptor mixDescriptor) {
    mixDescriptors.remove(mixDescriptor);

    if (Logger.logLevel >= Logger.LOG_MOREINFO) {
        Logger.println("Call " + member
        + " removeMix removed " + mixDescriptor);
    }
    setUseFastMix();
    }

    public void setAttenuation(MixDescriptor md, double attenuation) {
    md.setAttenuation(attenuation);

    setUseFastMix();
    }

    public void setMuted(MixDescriptor md, boolean isMuted) {
    md.setMuted(isMuted);
    setUseFastMix();
    }

    public MixDescriptor findMixDescriptor(MixDataSource mixDataSource) {
        for (int i = 0; i < mixDescriptors.size(); i++) {
            MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(i);

            if (mixDescriptor.getMixDataSource() == mixDataSource) {
                return mixDescriptor;
            }
        }

        if (forcePrivateMix) {
            double[] volume = new double[4];

            volume[0] = .5D;
            volume[1] = .5D;
            volume[2] = .5D;
            volume[3] = .5D;

            return new MixDescriptor(mixDataSource, 1.0, volume);
        }

        return null;
    }

    private void setUseFastMix() {
        useFastMix = false;

    if (mixDescriptors.size() != 2) {
        if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
            Logger.println("Call " + member +
            " Can't use fastMix, must have exactly 2 descriptors");
        }
        return;
    }

    for (int i = 0; i < 2; i++) {
        MixDescriptor mixDescriptor = (MixDescriptor)
            mixDescriptors.get(i);

        if (mixDescriptor.isMuted()) {
            if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
            Logger.println("Call " + member
            + " Can't use fastMix, md muted " + mixDescriptor);
        }
        return;
        }

        MixDataSource mixDataSource = mixDescriptor.getMixDataSource();

        if (mixDataSource instanceof MemberReceiver) {
            if (mixDataSource != member.getMemberReceiver()) {
                if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
                Logger.println("Call " + member
                + " Can't use fastMix, have private mix");
            }
            return;
            }

            if (mixDescriptor.getEffectiveVolume() != -1.0) {
                if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
                Logger.println("Call " + member
                + " Can't use fastMix, no mix minus");
            }
            return;
            }

        continue;
        } else if (mixDataSource instanceof WhisperGroup == false) {
            if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
            Logger.println("Call " + member
            + " Can't use fastMix, not simple mix");
        }
        return;
        }

        if (mixDescriptor.isNop() == false) {
            if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
            Logger.println(" Call " + member
            + "Can't use fastMix, not spatially neutral");
        }
        return;
        }
        }

    MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(0);

        if (mixDescriptor.getMixDataSource() instanceof MemberReceiver) {
        /*
         * Swap the two descriptors so that the conference mix is first
         */
        mixDescriptors.add(mixDescriptor);
        mixDescriptors.remove(0);
    }

    if (Logger.logLevel >= Logger.LOG_MOREDETAIL) {
        Logger.println("Using fastMix");
    }

    useFastMix = true;
    }

    public void showDescriptors() {
        Logger.println("Call " + member + " descriptors "
            + mixDescriptors.size());

        for (int i = 0; i < mixDescriptors.size(); i++) {
            MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(i);

        Logger.println(mixDescriptor.toString());
        }
    }

    public ArrayList getMixDescriptors() {
        return mixDescriptors;
    }

    /*
     * For load testing
     */
    private static boolean forcePrivateMix = false;

    public static void setForcePrivateMix(boolean forcePrivateMix) {
    MixManager.forcePrivateMix = forcePrivateMix;
    }

    public static boolean getForcePrivateMix() {
    return forcePrivateMix;
    }

    public MixDescriptor setPrivateMix(MixDataSource mixDataSource,
        double[] spatialValues) {

        MixDescriptor mixDescriptor;

    mixDescriptor = findMixDescriptor(mixDataSource);

    if (Logger.logLevel >= Logger.LOG_MOREINFO) {
        Logger.println("Private mix " + mixDescriptor);
    }

    if (mixDescriptor == null) {
        mixDescriptor = new MixDescriptor(mixDataSource, 1.0,
        spatialValues);

        mixDescriptors.add(mixDescriptor);

        Logger.println("Call " + member
        + " creating new private mix for " + mixDataSource + " "
        + mixDescriptor + " vol " + spatialValues[3]);
    } else {
        if (mixDescriptor.equals(mixDataSource, spatialValues)) {
        return null;	// same as before
        }

        mixDescriptor.setSpatialValues(spatialValues);
    }

        if (Logger.logLevel >= Logger.LOG_MOREINFO) {
        Logger.println("MixManager:  Setting private mix "
        + mixDescriptor);
    }

    setUseFastMix();
    return mixDescriptor;
    }

    public int[] mix() {
        int[] outData = null;

        if (mixDescriptors.size() == 0) {
            return null;
        }

        if (useFastMix) {
            if (Logger.logLevel >= Logger.LOG_INFO) {
                setUseFastMix();    // reset

                if (useFastMix == false) {
                    Logger.println("Call " + member
            + " useFastMix should have been false!  "
            + "resetting...");

                    Logger.println("Call " + member + " md size "
            + mixDescriptors.size());

            Logger.println(toAbbreviatedString());
                }
            }

        if (useFastMix) {
        return fastMix();
        }
    }

        outData = new int[conferenceSamplesPerPacket];

    //Logger.println("Call " + member + " MixManager mixing "
    //	+ mixDescriptors.size());

    boolean needToSend = false;

        for (int i = 0; i < mixDescriptors.size(); i++) {
            MixDescriptor mixDescriptor = (MixDescriptor) mixDescriptors.get(i);

        if (mixDescriptor.isMuted())  {
        continue;
        }

        MixDataSource mixDataSource = mixDescriptor.getMixDataSource();

            int[] contribution = mixDataSource.getCurrentContribution();

        if (mixDescriptor.isPrivateMix() == true) {
        double[] spatialValues = mixDescriptor.getSpatialValues();

        if (MixDescriptor.isSpatiallyNeutral(spatialValues) &&
            spatialValues[3] != 0) {

            /*
             * Since only the volume needs to be adjusted,
             * rather than subtracting out the contribution
             * and then adding in the volume we can
             * set the volume to volume - 1 and add that in.
             */
            if (mixDataSource.contributionIsInCommonMix()) {
                double[] sv = new double[4];

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

                spatialValues = sv;

                if (Logger.logLevel == -69) {
                Logger.println("Call " + member + " pm for "
                    + mixDataSource.toAbbreviatedString()
                    + " s3 " + spatialValues[3]);
            }
            }
        } else {
            /*
             * Subtract the current contribution from the mix
             */
            if (contribution != null &&
                mixDataSource.contributionIsInCommonMix()) {

                    WhisperGroup.mixData(contribution, outData, false);

                if (spatialValues[3] == 0) {
                if (Logger.logLevel == -44) {
                Logger.println("subtracted out "
                        + mixDescriptor);
                }
                continue;  // we've already subtracted it out
                }
            }
        }

        contribution = sa.generateSpatialAudio(
            mixDataSource.getSourceId(),
                    mixDataSource.getPreviousContribution(),
            contribution, spatialValues);
        }

            if (contribution != null) {
        /*
         * Mix into an int[] so that we can clip once after
         * we're done mixing.
         */
        boolean add = mixDescriptor.getEffectiveVolume() != -1;

                WhisperGroup.mixData(contribution, outData, add);
        needToSend = true;
            }
        }

    if (needToSend == false) {
        return null;
    }

    AudioConversion.clip(outData);

    if (Logger.logLevel == -39) {
        checkData(outData, false);
    }

        return outData;
    }

    /*
     * We know there are two MixDescriptors and the first one is
     * the conference Mix and the second one is for subtracting
     * out the member's own data.
     */
    private int[] fastMix() {
    MixDescriptor conferenceMixDescriptor = (MixDescriptor)
        mixDescriptors.get(0);

        int[] conferenceMixContribution =
        conferenceMixDescriptor.getMixDataSource().getCurrentContribution();

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

    int[] outData = new int[conferenceSamplesPerPacket];

    MixDescriptor memberMixDescriptor = (MixDescriptor)
        mixDescriptors.get(1);

        int[] memberContribution =
            memberMixDescriptor.getMixDataSource().getCurrentContribution();

        if (memberContribution == null) {

            if (outData.length <= conferenceMixContribution.length)
                System.arraycopy(conferenceMixContribution, 0, outData, 0, outData.length);
            else
                System.arraycopy(conferenceMixContribution, 0, outData, 0, conferenceMixContribution.length);

            if (Logger.logLevel == -39) {
                checkData(outData, useFastMix);
            }

            AudioConversion.clip(outData);
            return outData;
        }

        WhisperGroup.mixData(conferenceMixContribution, memberContribution,  outData);

        if (Logger.logLevel == -39) {
            checkData(outData, useFastMix);
        }

        AudioConversion.clip(outData);
        return outData;
    }

    private void checkData(int[] data, boolean useFastMix) {
    for (int i = 0; i < data.length; i++) {
        if (data[i] != 0) {
        Logger.println("Call " + member + " Non-zero data at " + i);
        Logger.println("Call " + member
            + " useFastMix " + useFastMix);
        Logger.println("Call " + member + " "
            + toAbbreviatedString());
        if (mixDescriptors.size() != 2 && useFastMix == true) {
            Logger.println("useFastMix should be false!!!");
        }
        break;
        }
    }
    }

    //public void adjustVolume(MixDescriptor md, double volume) {
    //	md.adjustSpatialVolume(volume);
    //}

    public void adjustVolume(int[] data, double volume) {
    if (volume == 1) {
        return;
    }

    if (volume == 0) {
        for (int i = 0; i < data.length; i++) {
        data[i] = 0;	// optimize when volume is 0
        }
        return;
    }

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

    public String toString() {
        String s = "";

        synchronized (this) {
            for (int i = 0; i < mixDescriptors.size(); i++) {
                MixDescriptor mixDescriptor = (MixDescriptor)
                    mixDescriptors.get(i);

                s += mixDescriptor.toString();
                s += "\n";
            }
        }

        return s;
    }

    public String toAbbreviatedString() {
        String s = "";

        synchronized (this) {
            for (int i = 0; i < mixDescriptors.size(); i++) {
                MixDescriptor mixDescriptor = (MixDescriptor)
                    mixDescriptors.get(i);

                s += "    " + mixDescriptor.toAbbreviatedString();

        if (member.getWhisperGroup() ==
                mixDescriptor.getMixDataSource()) {

            s += " + ";
        }

                s += "\n";
            }
        }

        return s;
    }

}
