/*
 * oggStreamEncoder is a class to insert an ogg packet into an ogg page stream
 *
 * Copyright (C) 2008 Joern Seger
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.

 * This program 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include <iostream>
#include <cstring>
#include <cstdlib>

#include "definition.h"
#include "oggStreamEncoder.h"
#include "oggHeader.h"
#include "crc.h"
#include "exception.h"
#include "log.h"

#define min(a,b) ((a<b)?(a):(b))
#define max(a,b) ((a>b)?(a):(b))

std::vector<uint32> OggStreamEncoder::usedSerialNo;

OggStreamEncoder::OggStreamEncoder(uint32 serial)
    : maxPageSize(4096), streamNo(0), dataLength(0), dataSegments(0), usedData(0), pageCounter(0)
//   packetCounter(0), positionInterpreterEnabled(false), pageKeepEnabled(false),
    //posInterpreter(0)
{
  uint32 newSerial = findUniqueSerial(serial);
  streamSerialNo = newSerial;
  setInitialized();
}

OggStreamEncoder::~OggStreamEncoder()
{
  if (!oggPacketList.empty())
    logger.warning() << "OggStreamEncoder::Destructor: WARNING packet list not empty ("<<oggPacketList.size()<<" Elements)\n";

  if (!oggPageList.empty())
    logger.warning() << "OggStreamEncoder::Destructor: WARNING page list not empty\n";

}

// we need a global repository to keep an Eye on the serial numbers
uint32 OggStreamEncoder::findUniqueSerial(uint32 origSerial)
{
  bool isUnique(false);
  uint32 serial;

  while (!isUnique) {
    serial     = (origSerial?origSerial:rand());
    isUnique   = true;
    origSerial = 0;

    for (uint32 i(0); i<usedSerialNo.size(); ++i)
      if (serial == usedSerialNo[i])
        isUnique = false;
  }
  usedSerialNo.push_back(serial);
  return(serial);
}

/*
void OggStreamEncoder::enablePositionInterpreter()
{
  positionInterpreterEnabled = true;
}

void OggStreamEncoder::keepOnePage()
{
  pageKeepEnabled = true;
}
*/

void OggStreamEncoder::addPacket(OggPacket& packet)
{
  /* if we want to interprete the position by ourself */
  /*
    if (positionInterpreterEnabled &&
        posInterpreter && (!packet.isStreamHeader()))
      posInterpreter->setStreamPosition(packet);
  */
  /* This is a normal packet
   * Let's start to calculate the actual length */
  oggPacketList.push_back(packet);

  dataLength             += packet.length();
  uint32 actSegmentsSize  = (packet.length()+255)/255;
  uint8  actSegments[maxSegmentEntries];

  if (actSegmentsSize > maxSegmentEntries)
    throw OggException("OggStreamEncoder::addPacket: Not able to handle this packet size");

  /* calculate the segment table part of this packet */
  memset(actSegments, 0xff, actSegmentsSize-1);
  actSegments[actSegmentsSize-1] = packet.length()%255;

  segmentsBuffer.addData(actSegments, actSegmentsSize);

}

bool OggStreamEncoder::getNextPacketLength(uint32 pageBorder, uint32& length,
    uint32& segments)
{
  /* initialize the values */
  length   = 0;
  segments = 0;

  /* if the data length is in range, do nothing */
  if ((dataLength < pageBorder) && (segmentsBuffer.getUsed() < 255)) {
    return(false);
  }

  /* get the actually available segments (maximum 255) */
  uint8 actSegments[255];
  uint32 actSegmentsSize = segmentsBuffer.peekFront(actSegments,255);

  /* and calculate, how many segments we want to include into the
   * actual page */
  for (uint32 count(0); count<actSegmentsSize; ++count) {
    length  += actSegments[count];
    segments++;
    if ((length >= pageBorder) || (segments >= 255)) {
      // this is a "sonderlocke" for zero size segments
      count++;
      while ((count < actSegmentsSize) && (actSegments[count] == 0)) {
        segments++;
        count++;
      }
      return(true);
    }
  }

  return(true);
}

OggStreamEncoder& OggStreamEncoder::operator<<(OggPacket packet)
{
  if (!isInitialized()) {
    logger.error() << "OggStreamEncoder::operator<<: Stream is not initialized correctly\n";
    return(*this);
  }

  if (!isConfigured()) {

    // this must be the bos packet
    if (!packet.isBOS()) {
      logger.error() << "OggStreamEncoder::operator<<: First packet must be a BOS packet\n";
      return(*this);
    } else {
      // The first BOS packet defines the stream number
      streamNo = packet.getStreamNo();
    }
    /* add the packet to the temporal buffer */
    addPacket(packet);

    /* the encoder stream is configured */
    setConfigured();

    /* flush the first packet (see Spec) */
    flush();

    return(*this);
  }

  /* add the packet to the temporal buffer */
  addPacket(packet);

  /* create as many packets as data is available */
  while ((dataLength >= maxPageSize) || (segmentsBuffer.getUsed() >= 255)) {
    createPage(maxPageSize);
  }

  return(*this);
}

OggStreamEncoder& OggStreamEncoder::operator>>(OggPage& page)
{
  if (isEmpty()) {
    logger.error() << "OggStreamEncoder::opertator>>: no page available\n";
    return(*this);
  }

  page = oggPageList.front();
  oggPageList.pop_front();

  page.obj()->streamNo = streamNo;

  if (oggPageList.empty())
    setEmpty();


  return(*this);

}

void OggStreamEncoder::flush()
{
  while (dataLength || (segmentsBuffer.getUsed() > 0)) {
    createPage(dataLength);
  }
}

void OggStreamEncoder::createPage(uint32 minPageSize)
{
  uint32 bodyLength;
  uint32 segmentsSize;

  /* Is there enough data available? */
  if (!getNextPacketLength(minPageSize, bodyLength, segmentsSize)) {
    return;
  }

  uint32 overallLength = sizeof(OggHeader) + segmentsSize + bodyLength;
  uint8* pageData = new uint8[overallLength];

  /* an ogg page looks like this:
   * --------------------------------------------------------
   * | Ogg Header | Segments Table | Packet1 | Packet2 | ...
   * --------------------------------------------------------
   */
  OggHeader* header   ((OggHeader*)pageData);
  uint8*     segments (pageData+sizeof(OggHeader));
  uint8*     body     (segments+segmentsSize);

  /* set the header information */
#ifdef HAVE_BZERO
  bzero(header,sizeof(OggHeader));
#else
  for (uint32 i(0); i<sizeof(OggHeader); ++i) *(((uint8*)header)+i) = 0;
#endif

  memcpy(header->ogg,"OggS",4);
  header->tableSegments = segmentsSize;
  header->pageNo        = pageCounter++;
  header->serial        = streamSerialNo;
  header->position      = -1;

  if (usedData)
    header->pack_type   = 1; // is packet starts on the previous page

  /* fill the segments */
  segmentsBuffer.getData(segments,segmentsSize);

  std::list<OggPacket>::iterator it(oggPacketList.begin());

  /* if this is a bos packet, this would surely be the first packet */
  if (it->isBOS())
    header->page_type = 1;

  uint32 arrayIndex(0);

  for (; it != oggPacketList.end(); it++) {

    uint32 cpyLength = min((it->length()-usedData),(bodyLength-arrayIndex));

    memcpy(body+arrayIndex, it->data()+usedData, cpyLength);
    arrayIndex += cpyLength;

    /* is this the end of the actual page */
    if (arrayIndex == bodyLength) {

      /* the packet does not start within this page and does not end
       * on this page? */
      if (usedData != 0) {
        usedData += cpyLength;
        if (usedData == it->length()) {
          usedData = 0;
          if (it->isEOS())
            header->last = 1;

          /* the packet is fully used, so point to the next valid packet */
          ++it; // this might be the .end() sign, however, how cares

        }

      } else {

        if (cpyLength == it->length()) {

          /* this packet ended on this page */
          usedData = 0;

          /* at lease the actual Packet has been completed on this page */
//          onePacketCompleted = true;

          /* Is it the end of a stream? Then mark it as such */
          if (it->isEOS())
            header->last = 1;

          /* the packet is fully used, so point to the next valid packet */
          ++it; // this might be the .end() sign, however, how cares

        } else {
          /* keep the number of copied bytes for the next page */
          usedData = cpyLength;
        }
      }

      // we found the end of the page
      if (usedData)
        break;
    }

    usedData = 0;
  }

#ifdef DEBUG
  if ( arrayIndex != bodyLength ) {
    logger.error() << "ERROR: array not matching ( index "<<arrayIndex <<" bodylength "<<bodyLength << ")\n";
    logger.error() << "SegmentsSize: "<<(int)segmentsSize<< " still available: "<<(int)segmentsBuffer.getUsed()<<"\n";
    for (uint32 i(0); i<segmentsSize; ++i)
      logger.error() << " 0x"<<(int)segments[i];
    logger.error() << "\n";
    abort();
  }
#endif

  /* set the granule position if at least one packet has ended on this page
   * The position is taken from the last full packet
   */
  if (oggPacketList.begin() != it) {
    std::list<OggPacket>::iterator it1 = it;
    it1--;
    header->position = it1->granulepos();
    oggPacketList.erase(oggPacketList.begin(), it);
  }

  header->checksum = Crc::create(pageData, overallLength);

  OggPage page(new OggPageInternal(pageData, sizeof(OggHeader)+segmentsSize, bodyLength));
  oggPageList.push_back(page);

  dataLength -= bodyLength;

  setAvailable();

}


