/*
 * 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.AudioConversion;
import com.sun.voip.CallParticipant;
import com.sun.voip.Logger;
import com.sun.voip.MediaInfo;
import com.sun.voip.MixDataSource;
import com.sun.voip.Recorder;
import com.sun.voip.RtpPacket;
import com.sun.voip.TreatmentManager;
import com.sun.voip.TreatmentDoneListener;
import com.sun.voip.Util;

import java.util.ArrayList;

import java.io.IOException;

import java.text.ParseException;

public class WhisperGroup implements MixDataSource, TreatmentDoneListener {
    private String id;

    private static double defaultAttenuation = .13;

    private static boolean commonMixDefault = true;

    private double attenuation;

    private boolean isTransient = false;
    private boolean isLocked = false;
    private boolean noCommonMix = false;

    private ArrayList members = new ArrayList();    // members in group
    private ArrayList whisperers = new ArrayList(); // members whispering

    private int[] linearMixBuffer;
    private int[] doNotRecordMix;

    private MediaInfo mediaInfo;

    public WhisperGroup(String id, double attenuation, MediaInfo mediaInfo) {
    this.id = id;
    this.attenuation = attenuation;
    this.mediaInfo = mediaInfo;

    if (commonMixDefault == false) {
        noCommonMix = true;
    }

    Logger.writeFile("New Whisper group:  " + toString());
    }

    public static void setCommonMixDefault(boolean commonMixDefault) {
    WhisperGroup.commonMixDefault = commonMixDefault;
    }

    public static boolean getCommonMixDefault() {
    return commonMixDefault;
    }

    public void setMediaInfo(MediaInfo mediaInfo) {
    this.mediaInfo = mediaInfo;

    setAttenuation(attenuation);  // reset attenuation
    }

    public String getId() {
    return id;
    }

    public double getAttenuation() {
    return attenuation;
    }

    public static double getDefaultAttenuation() {
    return defaultAttenuation;
    }

    public static void setDefaultAttenuation(double defaultAttenuation) {
    WhisperGroup.defaultAttenuation = defaultAttenuation;
    }

    public ArrayList getMembers() {
    return members;
    }

    public int whisperCount() {
    return whisperers.size();
    }

    public boolean isMember(ConferenceMember member) {
        return members.contains(member);
    }

    private void removeWhisperer(ConferenceMember member) {
    whisperers.remove(member);
    }

    /*
     * For debugging
     */
    public String checkMember(ConferenceMember member) {
    String s = "";

    if (members.contains(member) == false) {
        s += "\t**** Not a member of the whisper group! ****\n";
    }

    if (whisperers.contains(member) == false) {
        s += "\t**** Not in whisperers! ****\n";
    }

    return s;
    }

    public void addCall(ConferenceMember member) {
        if (members.contains(member) == true) {
        Logger.println(member + " is already in whisper group " + id);
        return;
    }

        members.add(member);
    }

    public void removeCall(ConferenceMember member) {
    if (members.contains(member) == false) {
        if (Logger.logLevel >= Logger.LOG_INFO) {
            Logger.println(member + " is not in whisper group " + id);
        }
        return;
    }

    members.remove(member);
    }

    public void setWhispering(boolean isWhispering, ConferenceMember member) {
    if (isMember(member) == false) {
        Logger.println("Call " + member
            + " is not a member of whisper group " + id);
        return;
    }

        if (isWhispering) {
        if (whisperers.contains(member)) {
        return;	  // already whispering
        }

        whisperers.add(member);

        if (Logger.logLevel >= Logger.LOG_INFO) {
            Logger.println("Call " + member + " started whispering to "
            + id);
        }
        return;
        }

    if (whisperers.contains(member) == false) {
        Logger.println("Call " + member + " is not in whisperers!");
        return;
    }

    removeWhisperer(member);

        if (Logger.logLevel >= Logger.LOG_INFO) {
        Logger.println("Call " + member + " stopped whispering to " + id);
    }
    }

    public void setTransient(boolean isTransient) {
    this.isTransient = isTransient;
    }

    public boolean isTransient() {
    return isTransient;
    }

    public void setLocked(boolean isLocked) {
    this.isLocked = isLocked;
    }

    public boolean isLocked() {
    return isLocked;
    }

    public void setAttenuation(double attenuation) {
    this.attenuation = attenuation;
    }

    public boolean hasCommonMix() {
    return noCommonMix == false;
    }

    public void setNoCommonMix(boolean noCommonMix) {
    this.noCommonMix = noCommonMix;
    }

    public void forwardDtmf(MemberReceiver memberReceiver, String dtmfKeys) {
    synchronized (members) {
        for (int i = 0; i < members.size(); i++) {
        ConferenceMember member = (ConferenceMember) members.get(i);

        if (member.getMemberReceiver() != memberReceiver) {
            member.getMemberSender().setDtmfKeyToSend(dtmfKeys);
        }
        }
    }
    }

    /*
     * This is called when data is received from a conference member
     * The member has already converted its contribution to linear.
     */
    public void addToLinearDataMix(int[] contribution, boolean doNotRecord)
    {
        if (doNotRecord) {
                if (doNotRecordMix == null) {
                    doNotRecordMix = new int[contribution.length];

                    System.arraycopy(contribution, 0, doNotRecordMix, 0,
                        contribution.length);
                    return;
                }

                mixData(contribution, doNotRecordMix, true);
            return;
        }

        if (linearMixBuffer == null) {
            linearMixBuffer = new int[contribution.length];

            System.arraycopy(contribution, 0, linearMixBuffer, 0,
                contribution.length);
            return;
        }

        mixData(contribution, linearMixBuffer, true);
    }

    public static void mixData(int[] inData, int[] mixData, boolean add)
    {
        //Logger.println("mixData - inData length " + inData.length  + " mixData length " + mixData.length + " add " + add);

        try {
            if (add) {

                if (inData.length <= mixData.length)
                {
                    for (int i = 0; i < inData.length; i++) {
                            mixData[i] = mixData[i] + inData[i];
                    }

                } else {

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

            } else {

                if (inData.length <= mixData.length)
                {
                    for (int i = 0; i < inData.length; i++) {
                            mixData[i] = mixData[i] - inData[i];
                    }
                } else {
                    for (int i = 0; i < mixData.length; i++) {
                            mixData[i] = mixData[i] - inData[i];
                    }
                }
            }

        } catch (IndexOutOfBoundsException e) {
            Logger.println("Exception!  inData length " + inData.length
            + " mixData length " + mixData.length + " add " + add);

            e.printStackTrace();
        }
    }

    public static void mixData(int[] conferenceData, int[] memberData, int[] outData)
    {
        //Logger.println("mixData - conferenceData length " + conferenceData.length +" memberData.length " + memberData.length + " outData length " + outData.length);

        try {
                if (outData.length <= memberData.length)
                {
                    for (int i = 0; i < outData.length; i ++) {
                        outData[i] = conferenceData[i] - memberData[i];
                    }

                } else {
                    for (int i = 0; i < memberData.length; i ++) {
                        outData[i] = conferenceData[i] - memberData[i];
                    }
                }

        } catch (IndexOutOfBoundsException e) {
            Logger.println("mixData Exception!  conferenceData length " + conferenceData.length +" memberData.length " + memberData.length + " outData length " + outData.length);

            e.printStackTrace();
        }
    }

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

    public String getSourceId() {
    return id;
    }

    public boolean contributionIsInCommonMix() {
    return hasCommonMix();
    }

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

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

    public void saveCurrentContribution() {
    currentContribution = linearMixBuffer;
    linearMixBuffer = null;

    if (currentTreatment != null) {
            synchronized (conferenceTreatments) {
            currentTreatment.saveCurrentContribution();

            int[] treatmentData = currentTreatment.getCurrentContribution();

        if (treatmentDone) {
                conferenceTreatments.remove(currentTreatment);

                startNextTreatment();
        }

            if (treatmentData != null) {
                if (currentContribution == null) {
                currentContribution = treatmentData;
                } else {
                    mixData(treatmentData, currentContribution, true);
                }
        }
        }
    }

    if (currentContribution != null) {
        recordAudio(currentContribution, currentContribution.length);
    }

    if (doNotRecordMix != null) {
        mixData(doNotRecordMix, currentContribution, true);
        doNotRecordMix = null;
    }
    }

    ArrayList conferenceTreatments = new ArrayList();
    TreatmentManager currentTreatment;
    boolean treatmentDone;

    public void addTreatment(TreatmentManager treatmentManager) {
    synchronized (conferenceTreatments) {
        conferenceTreatments.add(treatmentManager);

        if (currentTreatment == null) {
            startNextTreatment();
        }
    }
    }

    public void pauseTreatment(String treatment, boolean isPaused) {
        synchronized(conferenceTreatments) {
            for (int i = 0; i < conferenceTreatments.size(); i++) {
                TreatmentManager treatmentManager = (TreatmentManager)
                    conferenceTreatments.get(i);

                if (treatmentManager.getId().equals(treatment)) {
            treatmentManager.pause(isPaused);
            break;
        }
        }
    }
    }

    public void removeTreatment(String treatment) {
    synchronized(conferenceTreatments) {
        for (int i = 0; i < conferenceTreatments.size(); i++) {
        TreatmentManager treatmentManager = (TreatmentManager)
            conferenceTreatments.get(i);

        if (treatmentManager.getId().equals(treatment)) {
            if (currentTreatment == treatmentManager) {
                treatmentManager.stopTreatment();
            } else {
                conferenceTreatments.remove(treatmentManager);
            }
            break;
        }
        }
    }
    }

    public void treatmentDoneNotification(TreatmentManager treatmentManager) {
    /*
     * We cannot start the next treatment just yet because we got called
     * by saveCurrentContribution() above and we still need to use
     * current contribution to get the last chunk of data.
     */
    treatmentDone = true;
    }

    private void startNextTreatment() {
    treatmentDone = false;

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

        currentTreatment = (TreatmentManager) conferenceTreatments.get(0);
        currentTreatment.addTreatmentDoneListener(this);

        if (Logger.logLevel >= Logger.LOG_INFO) {
            Logger.println("whisper group " + id
                + " Starting first treatment " + currentTreatment.getId());
        }
    }

    private Recorder audioRecorder;
    private String recordingFile;
    private Integer recordingLock = new Integer(0);

    public void recordConference(boolean enabled, String recordingFile,
        String recordingType) throws IOException {

        if (enabled == false) {
        synchronized (recordingLock) {
            if (audioRecorder != null) {
            audioRecorder.done();
                audioRecorder = null;
            }

        this.recordingFile = null;
        }
        return;
    }

    if (audioRecorder == null) {
        synchronized (recordingLock) {
                audioRecorder = new Recorder(recordingFile, "au",
            mediaInfo);

        Logger.println("starting conference recorder for "
            + recordingFile + " " + mediaInfo);

        this.recordingFile = recordingFile;
        }
    }
    }

    public String getRecordingFile() {
    return recordingFile;
    }

    private void recordAudio(int[] data, int length) {
    synchronized (recordingLock) {
        if (audioRecorder == null) {
            return;
        }

        /*
         * We have linear data to record.
         * If the mediaInfo is PCMU, convert to PCMU before recording.
         */
            try {
            if (mediaInfo.getEncoding() != RtpPacket.PCMU_ENCODING) {
                    audioRecorder.write(data, 0, length);
        } else {
            byte[] ulawData = new byte[data.length];

            AudioConversion.linearToUlaw(data, ulawData, 0);
            audioRecorder.write(ulawData, 0, ulawData.length);
        }
            } catch (IOException e) {
                Logger.println("Unable to record data " + e.getMessage());
                audioRecorder = null;
            }
    }
    }

    public String toAbbreviatedString() {
    return toAbbreviatedString(false);
    }

    public String toAbbreviatedString(boolean showMembers) {
    String id = this.id;

    if (id.length() >= 14) {
        id = id.substring(0, 13);
    }

    String s = id + ":";

        s += (Math.round(attenuation * 1000) / 1000D);

    if (isTransient) {
        s += " Transient";
    }

    if (isLocked) {
        s += " Locked";
    }

    if (noCommonMix) {
        s += " NoCommonMix";
    }

        if (showMembers == false) {
        return s;
    }

    s += " ";

        for (int j = 0; j < members.size(); j++) {
            ConferenceMember member = (ConferenceMember) members.get(j);

            CallParticipant cp = member.getCallParticipant();

            id = cp.toConsiseString();

            s += "'" + id;

            if (whisperers.contains(member)) {
        s += "+";
            }

            s += "' ";
        }

        return s;
    }

    public String toString() {
    String s = id + ":";

    s += mediaInfo + " ";

    s += (Math.round(attenuation * 1000) / 1000D);

    if (isTransient) {
        s += " Transient";
    }

    if (isLocked) {
        s += " Locked";
    }

    s += " ";

    for (int i = 0; i < members.size(); i++) {
        ConferenceMember member =
        (ConferenceMember)members.get(i);

        CallParticipant cp = member.getCallParticipant();
        s += cp.getCallId();

        if (whisperers.contains(member)) {
        s += "+";
        }

        s += " ";
    }

    return s;
    }

}
