/**********
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 3 of the License, or (at your
option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.)

This library 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 Lesser General Public License for
more details.

You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
**********/
// "liveMedia"
// Copyright (c) 1996-2026 Live Networks, Inc.  All rights reserved.
// A class that encapsulates an Ogg file.
// Implementation

#include "OggFileParser.hh"
#include "OggDemuxedTrack.hh"
#include "ByteStreamFileSource.hh"
#include "VorbisAudioRTPSink.hh"
#include "SimpleRTPSink.hh"
#include "TheoraVideoRTPSink.hh"

////////// OggTrackTable definition /////////

// For looking up and iterating over the file's tracks:

class OggTrackTable {
public:
  OggTrackTable();
  virtual ~OggTrackTable();

  void add(OggTrack* newTrack);
  OggTrack* lookup(u_int32_t trackNumber);

  unsigned numTracks() const;

private:
  friend class OggTrackTableIterator;
  HashTable* fTable;
};


////////// OggFile implementation //////////

void OggFile::createNew(UsageEnvironment& env, char const* fileName,
			onCreationFunc* onCreation, void* onCreationClientData) {
  new OggFile(env, fileName, onCreation, onCreationClientData);
}

OggTrack* OggFile::lookup(u_int32_t trackNumber) {
  return fTrackTable->lookup(trackNumber);
}

struct DemuxRecord {
  OggDemux* demux;
  OggDemuxOnDeletionFunc* onDeletionFunc;
  void* objectToNotify;
};

OggDemux* OggFile
::newDemux(OggDemuxOnDeletionFunc* onDeletionFunc, void* objectToNotify) {
  OggDemux* demux = new OggDemux(*this);

  DemuxRecord* demuxRecord = new DemuxRecord();
  demuxRecord->demux = demux;
  demuxRecord->onDeletionFunc = onDeletionFunc;
  demuxRecord->objectToNotify = objectToNotify;

  fDemuxesTable->Add((char const*)demux, demuxRecord);

  return demux;
}

void OggFile::removeDemux(OggDemux* demux) {
  DemuxRecord* demuxRecord = (DemuxRecord*)(fDemuxesTable->Lookup((char const*)demux));
  if (demuxRecord != NULL) {
    fDemuxesTable->Remove((char const*)demux);

    if (demuxRecord->onDeletionFunc != NULL) {
      (*demuxRecord->onDeletionFunc)(demuxRecord->objectToNotify, demux);
    }
    delete demuxRecord;
  }
}

unsigned OggFile::numTracks() const {
  return fTrackTable->numTracks();
}

FramedSource* OggFile
::createSourceForStreaming(FramedSource* baseSource, u_int32_t trackNumber,
                           unsigned& estBitrate, unsigned& numFiltersInFrontOfTrack) {
  if (baseSource == NULL) return NULL;

  FramedSource* result = baseSource; // by default
  numFiltersInFrontOfTrack = 0; // by default

  // Look at the track's MIME type to set its estimated bitrate (for use by RTCP).
  // (Later, try to be smarter about figuring out the bitrate.) #####
  // Some MIME types also require adding a special 'framer' in front of the source.
  OggTrack* track = lookup(trackNumber);
  if (track != NULL) { // should always be true
    estBitrate = track->estBitrate;
  }

  return result;
}

RTPSink* OggFile
::createRTPSinkForTrackNumber(u_int32_t trackNumber, Groupsock* rtpGroupsock,
                              unsigned char rtpPayloadTypeIfDynamic) {
  OggTrack* track = lookup(trackNumber);
  if (track == NULL || track->mimeType == NULL) return NULL;

  RTPSink* result = NULL; // default value for unknown media types

  if (strcmp(track->mimeType, "audio/VORBIS") == 0) {
    // For Vorbis audio, we use the special "identification", "comment", and "setup" headers
    // that we read when we initially read the headers at the start of the file:
    result = VorbisAudioRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic,
					   track->samplingFrequency, track->numChannels,
					   track->vtoHdrs.header[0], track->vtoHdrs.headerSize[0],
					   track->vtoHdrs.header[1], track->vtoHdrs.headerSize[1],
					   track->vtoHdrs.header[2], track->vtoHdrs.headerSize[2]);
  } else if (strcmp(track->mimeType, "audio/OPUS") == 0) {
    result = SimpleRTPSink
      ::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic,
		  48000, "audio", "OPUS", 2, False/*only 1 Opus 'packet' in each RTP packet*/);
  } else if (strcmp(track->mimeType, "video/THEORA") == 0) {
    // For Theora video, we use the special "identification", "comment", and "setup" headers
    // that we read when we initially read the headers at the start of the file:
    result = TheoraVideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic,
					   track->vtoHdrs.header[0], track->vtoHdrs.headerSize[0],
					   track->vtoHdrs.header[1], track->vtoHdrs.headerSize[1],
					   track->vtoHdrs.header[2], track->vtoHdrs.headerSize[2]);
  }

  return result;
}


OggFile::OggFile(UsageEnvironment& env, char const* fileName,
		 onCreationFunc* onCreation, void* onCreationClientData)
  : Medium(env),
    fFileName(strDup(fileName)),
    fOnCreation(onCreation), fOnCreationClientData(onCreationClientData) {
  fTrackTable = new OggTrackTable;
  fDemuxesTable = HashTable::create(ONE_WORD_HASH_KEYS);

  FramedSource* inputSource = ByteStreamFileSource::createNew(envir(), fileName);
  if (inputSource == NULL) {
    // The specified input file does not exist!
    fParserForInitialization = NULL;
    handleEndOfBosPageParsing(); // we have no file, and thus no tracks, but we still need to signal this
  } else {
    // Initialize ourselves by parsing the file's headers:
    fParserForInitialization
      = new OggFileParser(*this, inputSource, handleEndOfBosPageParsing, this);
  }
}

OggFile::~OggFile() {
  delete fParserForInitialization;

  // Delete any outstanding "OggDemux"s, and the table for them:
  DemuxRecord* demuxRecord;
  while ((demuxRecord = (DemuxRecord*)fDemuxesTable->RemoveNext()) != NULL) {
    delete demuxRecord->demux;
    delete demuxRecord;
  }
  delete fDemuxesTable;
  delete fTrackTable;

  delete[] (char*)fFileName;
}

void OggFile::handleEndOfBosPageParsing(void* clientData) {
  ((OggFile*)clientData)->handleEndOfBosPageParsing();
}

void OggFile::handleEndOfBosPageParsing() {
  // Delete our parser, because it's done its job now:
  delete fParserForInitialization; fParserForInitialization = NULL;

  // Finally, signal our caller that we've been created and initialized:
  if (fOnCreation != NULL) (*fOnCreation)(this, fOnCreationClientData);
}

void OggFile::addTrack(OggTrack* newTrack) {
  fTrackTable->add(newTrack);
}


////////// OggTrackTable implementation /////////

OggTrackTable::OggTrackTable()
  : fTable(HashTable::create(ONE_WORD_HASH_KEYS)) {
}

OggTrackTable::~OggTrackTable() {
  // Remove and delete all of our "OggTrack" descriptors, and the hash table itself:
  OggTrack* track;
  while ((track = (OggTrack*)fTable->RemoveNext()) != NULL) {
    delete track;
  }
  delete fTable;
}

void OggTrackTable::add(OggTrack* newTrack) {
  OggTrack* existingTrack
    = (OggTrack*)fTable->Add((char const*)newTrack->trackNumber, newTrack);
  delete existingTrack; // if any
}

OggTrack* OggTrackTable::lookup(u_int32_t trackNumber) {
  return (OggTrack*)fTable->Lookup((char const*)trackNumber);
}

unsigned OggTrackTable::numTracks() const { return fTable->numEntries(); }

OggTrackTableIterator::OggTrackTableIterator(OggTrackTable& ourTable) {
  fIter = HashTable::Iterator::create(*(ourTable.fTable));
}

OggTrackTableIterator::~OggTrackTableIterator() {
  delete fIter;
}

OggTrack* OggTrackTableIterator::next() {
  char const* key;
  return (OggTrack*)fIter->next(key);
}


////////// OggTrack implementation //////////

OggTrack::OggTrack()
  : trackNumber(0), mimeType(NULL),
    samplingFrequency(48000), numChannels(2), estBitrate(100) { // default settings
  vtoHdrs.header[0] = vtoHdrs.header[1] = vtoHdrs.header[2] = NULL;
  vtoHdrs.headerSize[0] = vtoHdrs.headerSize[1] = vtoHdrs.headerSize[2] = 0;

  vtoHdrs.vorbis_mode_count = 0;
  vtoHdrs.vorbis_mode_blockflag = NULL;
}

OggTrack::~OggTrack() {
  delete[] vtoHdrs.header[0]; delete[] vtoHdrs.header[1]; delete[] vtoHdrs.header[2];
  delete[] vtoHdrs.vorbis_mode_blockflag;
}


///////// OggDemux implementation /////////

FramedSource* OggDemux::newDemuxedTrack(u_int32_t& resultTrackNumber) {
  OggTrack* nextTrack;
  do {
    nextTrack = fIter->next();
  } while (nextTrack != NULL && nextTrack->mimeType == NULL);

  if (nextTrack == NULL) { // no more tracks
    resultTrackNumber = 0;
    return NULL;
  }

  resultTrackNumber = nextTrack->trackNumber;
  FramedSource* trackSource = new OggDemuxedTrack(envir(), resultTrackNumber, *this);
  fDemuxedTracksTable->Add((char const*)resultTrackNumber, trackSource);
  return trackSource;
}

FramedSource* OggDemux::newDemuxedTrackByTrackNumber(unsigned trackNumber) {
  if (trackNumber == 0) return NULL;

  FramedSource* trackSource = new OggDemuxedTrack(envir(), trackNumber, *this);
  fDemuxedTracksTable->Add((char const*)trackNumber, trackSource);
  return trackSource;
}

OggDemuxedTrack* OggDemux::lookupDemuxedTrack(u_int32_t trackNumber) {
  return (OggDemuxedTrack*)fDemuxedTracksTable->Lookup((char const*)trackNumber);
}

OggDemux::OggDemux(OggFile& ourFile)
  : Medium(ourFile.envir()),
    fOurFile(ourFile), fDemuxedTracksTable(HashTable::create(ONE_WORD_HASH_KEYS)),
    fIter(new OggTrackTableIterator(*fOurFile.fTrackTable)) {
  FramedSource* fileSource = ByteStreamFileSource::createNew(envir(), ourFile.fileName());
  fOurParser = new OggFileParser(ourFile, fileSource, handleEndOfFile, this, this);
}

OggDemux::~OggDemux() {
  // Begin by acting as if we've reached the end of the source file.
  // This should cause all of our demuxed tracks to get closed.
  handleEndOfFile();

  // Then delete our table of "OggDemuxedTrack"s
  // - but not the "OggDemuxedTrack"s themselves; that should have already happened:
  delete fDemuxedTracksTable;

  delete fIter;
  delete fOurParser;
  fOurFile.removeDemux(this);
}

void OggDemux::removeTrack(u_int32_t trackNumber) {
  fDemuxedTracksTable->Remove((char const*)trackNumber);
  if (fDemuxedTracksTable->numEntries() == 0) {
    // We no longer have any demuxed tracks, so delete ourselves now:
    Medium::close(this);
  }
}

void OggDemux::continueReading() {
  fOurParser->continueParsing();
}

void OggDemux::handleEndOfFile(void* clientData) {
  ((OggDemux*)clientData)->handleEndOfFile();
}

void OggDemux::handleEndOfFile() {
  // Iterate through all of our 'demuxed tracks', handling 'end of input' on each one.
  // Hack: Because this can cause the hash table to get modified underneath us,
  // we don't call the handlers until after we've first iterated through all of the tracks.
  unsigned numTracks = fDemuxedTracksTable->numEntries();
  if (numTracks == 0) return;
  OggDemuxedTrack** tracks = new OggDemuxedTrack*[numTracks];

  HashTable::Iterator* iter = HashTable::Iterator::create(*fDemuxedTracksTable);
  unsigned i;
  char const* trackNumber;

  for (i = 0; i < numTracks; ++i) {
    tracks[i] = (OggDemuxedTrack*)iter->next(trackNumber);
  }
  delete iter;

  for (i = 0; i < numTracks; ++i) {
    if (tracks[i] == NULL) continue; // sanity check; shouldn't happen
    tracks[i]->handleClosure();
  }

  delete[] tracks;
}
