/*
 * conference.cxx
 *
 * Conferencing functions for a simple MCU
 *
 * Copyright (c) 2000 Equivalence Pty. Ltd.
 * Copyright (c) 2004 Post Increment
 *
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * The Original Code is Portable Windows Library.
 *
 * The Initial Developer of the Original Code is Equivalence Pty. Ltd.
 *
 * Portions of this code were written by Post Increment (http://www.postincrement.com) 
 * with the assistance of funding from Citron Networks (http://www.citron.com.tw)
 *
 * Portions are Copyright (C) 1993 Free Software Foundation, Inc.
 * All Rights Reserved.
 *
 * Contributor(s): Derek J Smithies (derek@indranet.co.nz)
 *                 ------------------------------
 *
 * $Log: conference.cxx,v $
 * Revision 2.5  2004/09/22 22:39:26  csoutheren
 * Fixed race condition, thanks to Neil McCurdy
 *
 * Revision 2.4  2004/05/26 06:54:31  csoutheren
 * Changed to be a PHTTPServiceProcess
 * Added ability to play WAV files on member entry and exit
 * Added additional documentation on all classes
 * Preparation for re-introducing video
 *
 * Revision 2.3  2004/03/31 03:36:38  csoutheren
 * Fixed problem with user indication messages
 * Fixed problems with room listener and unlisten
 *
 * Revision 2.2  2004/03/30 11:27:23  csoutheren
 * Reverted to old mixing algorithm
 *
 * Revision 2.1  2004/03/23 11:40:06  csoutheren
 * Fixed problem where deleting map element in-place causes crash at end of call
 * Fixed problem where referencing map by iterator rather than ID
 * Fixed code formatting problems
 *
 * Revision 2.0  2004/03/08 02:06:24  csoutheren
 * Totally rewritten to use new connection locking mecahnism
 * Added ability to monitor conferences
 * Added initial support for H.323 MCU messages
 * Thanks to Citron Networks for supporting this work
 *
 */

#include <ptlib.h>

#include "conference.h"

// size of a PCM data packet, in samples
#define PCM_PACKET_LEN          240

// size of a PCM data buffer, in bytes
#define PCM_BUFFER_LEN          (PCM_PACKET_LEN * 2)

// number of PCM buffers to keep
#define PCM_BUFFER_COUNT        2

#define PCM_BUFFER_SIZE         (PCM_BUFFER_LEN * PCM_BUFFER_COUNT)

////////////////////////////////////////////////////////////////////////////////////

ConferenceManager::ConferenceManager()
{
}

Conference * ConferenceManager::MakeConference(const PString & roomToCreate, const PString & name)
{
  OpalGloballyUniqueID conferenceID;
  {
    PWaitAndSignal m(conferenceListMutex);
    ConferenceList::const_iterator r;
    for (r = conferenceList.begin(); r != conferenceList.end(); ++r) {
      if (roomToCreate == r->second->GetNumber()) {
        conferenceID = r->second->GetID();
        break;
      }
    }
  }

  return MakeConference(conferenceID, roomToCreate, name);
}


Conference * ConferenceManager::MakeConference(const OpalGloballyUniqueID & conferenceID, 
                                                 const PString & roomToCreate, 
                                                 const PString & name)
{
  Conference * conference = NULL;
  BOOL newConference = FALSE;
  {
    PWaitAndSignal m(conferenceListMutex);
    ConferenceList::const_iterator r = conferenceList.find(conferenceID);
    if (r != conferenceList.end())
      conference = r->second;
    else {
      conference = CreateConference(conferenceID, roomToCreate, name);
      conferenceList.insert(std::pair<OpalGloballyUniqueID, Conference *>(conferenceID, conference));
      newConference = TRUE;
    }
  }

  if (newConference)
    OnCreateConference(conference);

  return conference;
}

Conference * ConferenceManager::CreateConference(const OpalGloballyUniqueID & _guid,
                                                              const PString & _number,
                                                              const PString & _name)
{ 
  return new Conference(*this, _guid, _number, _name); 
}


BOOL ConferenceManager::HasConference(const OpalGloballyUniqueID & conferenceID)
{
  PWaitAndSignal m(conferenceListMutex);
  ConferenceList::const_iterator r = conferenceList.find(conferenceID);
  return r != conferenceList.end();
}

BOOL ConferenceManager::HasConference(const PString & number)
{
  PWaitAndSignal m(conferenceListMutex);
  ConferenceList::const_iterator r;
  for (r = conferenceList.begin(); r != conferenceList.end(); ++r) {
    if (r->second->GetNumber() == number)
      return TRUE;
  }
  return FALSE;
}

void ConferenceManager::RemoveConference(const OpalGloballyUniqueID & confId)
{
  PWaitAndSignal m(conferenceListMutex);
  ConferenceList::iterator r = conferenceList.find(confId);
  if (r != conferenceList.end())  {
    Conference * conf = r->second;
    OnDestroyConference(conf);
    conferenceList.erase(confId);
    delete conf;
  }
}

void ConferenceManager::RemoveMember(const OpalGloballyUniqueID & confId, ConferenceMember * toRemove)
{
  Conference * conf = toRemove->GetConference();
  BOOL removeConf = conf->RemoveMember(toRemove);
  delete toRemove;
  if (removeConf)
    RemoveConference(conf->GetID());
}

////////////////////////////////////////////////////////////////////////////////////

Conference::Conference(        ConferenceManager & _manager,
                       const OpalGloballyUniqueID & _guid,
                                    const PString & _number,
                                    const PString & _name)
  : manager(_manager), guid(_guid), number(_number), name(_name)
{ 
  //memberList.DisallowDeleteObjects();

  PTRACE(3, "Conference\tNew conference started: ID=" << guid << ", number = " << number);

#ifndef NO_MCU_VIDEO

  // if this is the first connection to a room, create the video buffer.
  if (create_new_room) {
    //cout << "CREATING VIDEO BUFFER FOR "<<newRoomID<<endl;
    videoBufferDict.SetAt(newRoomID, new VideoBuffer);

    VideoBuffer & videoBuffer = videoBufferDict[newRoomID];
    if (videoLarge)
      videoBuffer.SetSize(352,288);


    // create the spoken list.
    spokenListDict.SetAt(newRoomID, new PStringList);
    if (singleStream) {
      PStringList & spokenList = spokenListDict[newRoomID];
      spokenList.AppendString(newToken);
    }


    // create the position list.
    videoPosnDict.SetAt(newRoomID, new PStringList);

    PStringList & videoPosnList = videoPosnDict[newRoomID];

    if (singleStream) {
      videoPosnList.AppendString("");
    } else {
      PINDEX i;
      for(i=0;i<4;i++)  // hard coded to support 4 images
        videoPosnList.AppendString("");
    }
  }


  // Normally we display the video for the "active" users, ie
  // people who are currently speaking (based on noise detection)
  //
  // If there are currently less than 4 members then we can display
  // the video right from the start, before they begin talking.
  // This is also handy for connections with video, but no audio.
  //
  // Problem is finding out whether or not an ep sends video.
  // That is for a coming version.

  if (singleStream) {
    //only first connection must send video
    if (create_new_room) {
      AddVideoPosnToken(newToken,newRoomID);
    }
  } else {
    AddVideoPosnToken(newToken,newRoomID);
  }

#endif
}

Conference::~Conference()
{
}

void Conference::AddMember(ConferenceMember * memberToAdd)
{
  PTRACE(3, "Conference\tAdding member " << memberToAdd->GetName() << " to conference " << guid);
  cout << memberToAdd->GetName() << " joining conference " << number << "(" << guid << ")" << endl;

  // add the member to the conference
  memberToAdd->AddToConference(this);

  {
    // lock the member list
    PWaitAndSignal m(memberListMutex);

    // add this member to the conference member list
    memberList.insert(std::pair<void *, ConferenceMember *>(memberToAdd->GetID(), memberToAdd));

    // make sure each member has a connection created for the new member
    // make sure the new member has a connection created for reach existing member
    std::map<void *, ConferenceMember *>::const_iterator r;
    for (r = memberList.begin(); r != memberList.end(); r++) {
      ConferenceMember * conn = r->second;
      if (conn != memberToAdd) {
        conn->AddConnection(memberToAdd);
        memberToAdd->AddConnection(conn);
      }
    }
  }

  // call the callback function
  OnMemberJoining(memberToAdd);
}

BOOL Conference::RemoveMember(ConferenceMember * memberToRemove)
{
  PTRACE(3, "Conference\tRemoving call " << memberToRemove->GetName() << " from conference " << guid << " with size " << (PINDEX)memberList.size());
  cout << memberToRemove->GetName() << " leaving conference " << number << "(" << guid << ")" << endl;

  BOOL result;
  {
    PWaitAndSignal m(memberListMutex);

    // remove this member from the connection lists for all other members
    std::map<void *, ConferenceMember *>::iterator r;
    for (r = memberList.begin(); r != memberList.end(); r++) {
      ConferenceMember * conn = r->second;
      if (conn != memberToRemove) 
        r->second->RemoveConnection(memberToRemove->GetID());
    }

    // remove this connection from the member list
    memberList.erase(memberToRemove->GetID());

    // return TRUE if conference is empty
    result = memberList.size() == 0;
  }

  // call the callback function
  if (!result)
    OnMemberLeaving(memberToRemove);

  return result;
}

#ifndef NO_MCU_VIDEO
PINDEX Conference::FindTokensVideoPosn(const PString & thisToken,
                                           const PString & roomID)
{
  PStringList & videoPosnList = videoPosnDict[roomID];
  PINDEX keyIndex;
  for (keyIndex = 0; keyIndex<videoPosnList.GetSize(); keyIndex++)
    if (thisToken == videoPosnList[keyIndex] )
      return keyIndex;
  
  return P_MAX_INDEX;
}

BOOL Conference::AddVideoPosnToken(const PString & thisToken,
                                       const PString & roomID)
{
  PStringList & videoPosnList = videoPosnDict[roomID];
  PINDEX keyIndex;
  for (keyIndex = 0; keyIndex<videoPosnList.GetSize(); keyIndex++)
    if (videoPosnList[keyIndex] == "" ) {
      videoPosnList[keyIndex] = thisToken;
      return TRUE;
    }
  
  return FALSE;
}

#endif

#if 0
BOOL Conference::WriteAudio(OpenMCUH323Connection * source, const void * buffer, PINDEX amount)
{
  //The spokenList construct is required to determine who were the last
  //to speak, which is needed for determining which four videoimages are displayed.
  //spokenList contains a sorted list of when the last packet was received from
  //which connection. Thus, when an audio packet is received from a connection,
  //the name of the connection is moved to the end of the list.

  // If someone new comes along, everyone moves down.
#ifndef NO_MCU_VIDEO
  if ( DetectNoise(buffer,amount) && !singleStream ) {
    PStringList & spokenList = spokenListDict[roomID];
    PINDEX keyIndex = spokenList.GetStringsIndex(thisToken);
    if (keyIndex != P_MAX_INDEX)
      spokenList.RemoveAt(keyIndex);
    spokenList.AppendString(thisToken);  
    if (FindTokensVideoPosn(thisToken,roomID) != P_MAX_INDEX)
      goto processAudio;

    if (!singleStream && AddVideoPosnToken(thisToken,roomID))
      goto processAudio;
    
    keyIndex = spokenList.GetSize() - 1;
    PStringList & videoPosnList = videoPosnDict[roomID];
    if (keyIndex >= videoPosnList.GetSize()) {
      PString tokenToWipe = spokenList[keyIndex-videoPosnList.GetSize()];
      keyIndex = FindTokensVideoPosn(tokenToWipe,roomID);
    }
    if(keyIndex != P_MAX_INDEX)
      videoPosnList[keyIndex] = thisToken;
  }

    processAudio:
#endif

  PINDEX i;
  for (i = 0; i < memberList.GetSize(); i++)
    memberList.GetDataAt(i).WriteAudio(source, buffer, amount);

  return TRUE;
}
#endif

void Conference::OnMemberJoining(ConferenceMember * member)
{ 
  manager.OnMemberJoining(this, member); 
}

void Conference::OnMemberLeaving(ConferenceMember * member)
{ 
  manager.OnMemberLeaving(this, member); 
}


///////////////////////////////////////////////////////////////////////////

ConferenceMember::ConferenceMember(void * _id)
  : id(_id)
{
  conference = NULL;
}

BOOL ConferenceMember::AddToConference(Conference * _conference)
{
  if (conference != NULL)
    return FALSE;
  
  conference = _conference;
  return TRUE;
}

void ConferenceMember::RemoveFromConference()
{
  if (conference != NULL) {
    if (conference->RemoveMember(this))
      conference->GetManager().RemoveConference(conference->GetID());
  }
}

void ConferenceMember::AddConnection(ConferenceMember * memberToAdd)
{
  void * newID = memberToAdd->GetID();
  PTRACE(3, "Conference\tAdding " << newID << " to connection " << id);
  if (lock.Wait(TRUE)) {
    ConferenceConnection * conn = memberToAdd->CreateConnection();
    memberList.insert(std::pair<void *, ConferenceMember *>(newID, memberToAdd));
    connectionList.insert(std::pair<void *, ConferenceConnection *>(newID, conn));
    lock.Signal(TRUE);
  }
}

void ConferenceMember::RemoveConnection(void * idToDelete)
{
  PTRACE(3, "Conference\tRemoving member " << idToDelete << " from connection " << id);
  if (lock.Wait(TRUE)) {
    memberList.erase(idToDelete);
    connectionList.erase(idToDelete);
    lock.Signal(TRUE);
  }
}

void ConferenceMember::SendUserInputIndication(const PString & str)
{ 
  PTRACE(3, "Conference\tConnection " << id << " sending user indication " << str);
  if (lock.Wait()) {
    MemberList::iterator r;
    for (r = memberList.begin(); r != memberList.end(); ++r)
      r->second->OnReceivedUserInputIndication(str);
    lock.Signal();
  }
}

void ConferenceMember::WriteAudio(const void * buffer, PINDEX amount)
{
  if (lock.Wait()) {
    MemberList::iterator r;
    for (r = memberList.begin(); r != memberList.end(); ++r)
      r->second->OnExternalAudio(id, (BYTE *)buffer, amount);
    lock.Signal();
  }
}

void ConferenceMember::ReadAudio(void * buffer, PINDEX amount)
{
  // First, set the buffer to empty.
  memset(buffer, 0, amount);

  if (lock.Wait()) {

    // get number of channels to mix
    ConnectionList::iterator r;
    for (r = connectionList.begin(); r != connectionList.end(); ++r)
      r->second->ReadAndMixAudio((BYTE *)buffer, amount, (PINDEX)connectionList.size());

    lock.Signal();
  }
}

void ConferenceMember::OnExternalAudio(void * source, const void * buffer, PINDEX amount)
{
  if (lock.Wait()) {
    ConnectionList::iterator r = connectionList.find(source);
    if (r != connectionList.end())
      r->second->Write((BYTE *)buffer, amount);
    lock.Signal();
  }
}


#ifndef NO_MCU_VIDEO

virtual BOOL ConferenceMember::OnOutgoingVideo(void * buffer, PINDEX & amount)
{
}

virtual BOOL ConferenceMember::OnIncomingVideo(const void * buffer, PINDEX amount)
{
}

#endif

///////////////////////////////////////////////////////////////////////////

ConferenceConnection::ConferenceConnection(void * _id)
  : id(_id), bufferSize(PCM_BUFFER_SIZE)
{
  buffer = new BYTE[bufferSize];
  bufferStart = bufferLen = 0;
}

ConferenceConnection::~ConferenceConnection()
{
  delete[] buffer;
}

void ConferenceConnection::Write(const BYTE * data, PINDEX amount)
{
  if (amount == 0)
    return;

  PWaitAndSignal mutex(audioBufferMutex);
  
  // if there is not enough room for the new data, make room
  PINDEX newLen = bufferLen + amount;
  if (newLen > bufferSize) {
    PINDEX toRemove = newLen - bufferSize;
    bufferStart = (bufferStart + toRemove) % bufferSize;
    bufferLen -= toRemove;
  }

  // copy data to the end of the new data, up to the end of the buffer
  PINDEX copyStart = (bufferStart + bufferLen) % bufferSize;
  if ((copyStart + amount) > bufferSize) {
    PINDEX toCopy = bufferSize - copyStart;
    memcpy(buffer + copyStart, data, toCopy);
    copyStart = 0;
    data      += toCopy;
    amount    -= toCopy;
    bufferLen += toCopy;
  }

  // copy the rest of the data
  if (amount > 0) {
    memcpy(buffer + copyStart, data, amount);
    bufferLen   += amount;
  }
}


void ConferenceConnection::ReadAudio(BYTE * data, PINDEX amount)
{
  if (amount == 0)
    return;

  PWaitAndSignal mutex(audioBufferMutex);
  
  if (bufferLen == 0) {
    memset(data, 0, amount); // nothing in the buffer. return silence
    return;
  }

  // fill output data block with silence if audiobuffer is
  // almost empty.
  if (amount > bufferLen) 
    memset(data + bufferLen, 0, amount - bufferLen);

  // only copy up to the amount of data remaining
  PINDEX copyLeft = PMIN(amount, bufferLen);

  // if buffer is wrapping, get first part
  if ((bufferStart + copyLeft) > bufferSize) {
    PINDEX toCopy = bufferSize - bufferStart;

    memcpy(data, buffer + bufferStart, toCopy);

    data        += toCopy;
    bufferLen   -= toCopy;
    copyLeft    -= toCopy;
    bufferStart = 0;
  }

  // get the remainder of the buffer
  if (copyLeft > 0) {

    memcpy(data, buffer + bufferStart, copyLeft);

    bufferLen -= copyLeft;
    bufferStart = (bufferStart + copyLeft) % bufferSize;
  }
}

void ConferenceConnection::ReadAndMixAudio(BYTE * data, PINDEX amount, PINDEX channels)
{
  if (amount == 0) {
    PTRACE(3, "Mixer\tNo data to read");
    return;
  }

  PWaitAndSignal mutex(audioBufferMutex);
  
  if (bufferLen == 0) {
    // nothing in the buffer to mix.
    return;
  }

  // only mix up to the amount of data remaining
  PINDEX copyLeft = PMIN(amount, bufferLen);

  // if buffer is wrapping, get first part
  if ((bufferStart + copyLeft) > bufferSize) {
    PINDEX toCopy = bufferSize - bufferStart;

    Mix(data, buffer + bufferStart, toCopy, channels);

    data        += toCopy;
    bufferLen   -= toCopy;
    copyLeft    -= toCopy;
    bufferStart = 0;
  }

  // get the remainder of the buffer
  if (copyLeft > 0) {

    Mix(data, buffer + bufferStart, copyLeft, channels);

    bufferLen -= copyLeft;
    bufferStart = (bufferStart + copyLeft) % bufferSize;
  }
}

void ConferenceConnection::Mix(BYTE * dst, const BYTE * src, PINDEX count, PINDEX /*channels*/)
{
#if 0
  memcpy(dst, src, count);
#else
  PINDEX i;
  for (i = 0; i < count; i += 2) {

    int srcVal = *(short *)src;
    int dstVal = *(short *)dst;

    int newVal = dstVal;

#if 0     //The loudest person gains the channel.
#define mix_abs(x) ((x) >= 0 ? (x) : -(x))
    if (mix_abs(newVal) > mix_abs(srcVal))
      dstVal = newVal;
    else
      dstVal = srcVal; 
#else   //Just add up all the channels.
    if ((newVal + srcVal) > 0x7fff)
      dstVal = 0x7fff;
    else
      dstVal += srcVal;
#endif
    *(short *)dst = (short)dstVal;

    dst += 2;
    src += 2;
  }
#endif
}

///////////////////////////////////////////////////////////////

MCULock::MCULock()
{
  closing = FALSE;
  count = 0;
}

BOOL MCULock::Wait(BOOL hard)
{
  mutex.Wait();
  if (hard)
    return TRUE;

  BOOL ret = TRUE;
  if (!closing)
    count++;
  else
    ret = FALSE;

  mutex.Signal();
  return ret;
}

void MCULock::Signal(BOOL hard)
{
  if (hard) {
    mutex.Signal();
    return;
  }

  mutex.Wait();
  if (count > 0)
    count--;
  if (closing)
    closeSync.Signal();
  mutex.Signal();
}

void MCULock::WaitForClose()
{
  mutex.Wait();
  closing = TRUE;
  BOOL wait = count > 0;
  mutex.Signal();
  while (wait) {
    closeSync.Wait();
    mutex.Wait();
    wait = count > 0;
    mutex.Signal();
  }
}


syntax highlighted by Code2HTML, v. 0.9.1