/*
  ogminfo -- utility for extracting raw streams from an OGG media file

  Written by Moritz Bunkus <moritz@bunkus.org>
  Integrated into transcode by Tilmann Bitterberg <transcode@tibit.org>

  Distributed under the GPL
  see the file COPYING for details
  or visit http://www.gnu.org/copyleft/gpl.html
*/

#include "transcode.h"
#include "libtc/libtc.h"
#include "tcinfo.h"

#include <sys/types.h>

#include "ioaux.h"
#include "tc.h"

#define NOAUDIO 0
#define NOVIDEO 1
#define NOTEXT  2

unsigned char *xaudio = NULL;
unsigned char *xvideo = NULL;
unsigned char *xtext = NULL;
int no[3];
int      xraw = 0;

static int verbose_flag = TC_QUIET;

#if (HAVE_OGG && HAVE_VORBIS)

#include <ogg/ogg.h>
#include <vorbis/codec.h>

#include "ogmstreams.h"

#define BLOCK_SIZE 4096

#define ACVORBIS 0xffff
#define ACPCM    0x0001
#define ACMP3    0x0055
#define ACAC3    0x2000

typedef struct stream_t {
  int                 serial;
  int                 fd;
  double              sample_rate;
  int                 eos, comment;
  int                 sno;
  char                stype;
  ogg_stream_state    instate;
  struct stream_t    *next;

  int                 acodec;
  ogg_int64_t         bwritten;

  ogg_stream_state    outstate;
  int                 packetno;
  ogg_int64_t         max_granulepos;

  int                 subnr;
} stream_t;

stream_t *first;
int      nastreams = 0, nvstreams = 0, ntstreams = 0, numstreams = 0;
char basename[] = "stdout";

static void add_stream(stream_t *ndmx) {
  stream_t *cur = first;

  if (first == NULL) {
    first = ndmx;
    first->next = NULL;
  } else {
    cur = first;
    while (cur->next != NULL)
      cur = cur->next;
    cur->next = ndmx;
    ndmx->next = NULL;
  }
}

static stream_t *find_stream(int fserial) {
  stream_t *cur = first;

  while (cur != NULL) {
    if (cur->serial == fserial)
      break;
    cur = cur->next;
  }

  return cur;
}

double highest_ts = 0;

static int extraction_requested(unsigned char *s, int stream, int type) {
  int i;

  if (no[type])
    return 0;
  if (strlen((char *)s) == 0)
    return 1;
  for (i = 0; i < strlen((char *)s); i++)
    if (s[i] == stream)
      return 1;

  return 0;
}

static void flush_pages(stream_t *stream, ogg_packet *op) {
  ogg_page page;

  while (ogg_stream_flush(&stream->outstate, &page)) {
    int ih, ib;

    ih = tc_pwrite(stream->fd, page.header, page.header_len);
    if (ih != page.header_len) {
        tc_log_error(__FILE__, "error while writing page header");
        import_exit(1); /* XXX ugly */
    }
    ib = tc_pwrite(stream->fd, page.body, page.body_len);
    if (ib != page.body_len) {
        tc_log_error(__FILE__, "error while writing page bofy");
        import_exit(1); /* XXX ugly */
    }
    if (verbose_flag & TC_DEBUG)
      tc_log_msg(__FILE__, "x/a%d: %d + %d written", stream->sno, ih, ib);
  }
}

static void write_pages(stream_t *stream, ogg_packet *op) {
  ogg_page page;

  while (ogg_stream_pageout(&stream->outstate, &page)) {
    int ih, ib;

    ih = tc_pwrite(stream->fd, page.header, page.header_len);
    if (ih != page.header_len) {
        tc_log_error(__FILE__, "error while writing page header");
        import_exit(1); /* XXX ugly */
    }
    ib = tc_pwrite(stream->fd, page.body, page.body_len);
    if (ib != page.body_len) {
        tc_log_error(__FILE__, "error while writing page bofy");
        import_exit(1); /* XXX ugly */
    }
    if (verbose_flag & TC_DEBUG)
      tc_log_msg(__FILE__, "x/a%d: %d + %d written", stream->sno, ih, ib);
  }
}

static void handle_packet(stream_t *stream, ogg_packet *pack, ogg_page *page) {
  int i, w, hdrlen, end;
  long long lenbytes;
  char *sub;
  char out[100];
  ogg_int64_t pgp, sst;

  //tc_log_msg(__FILE__, "Going handle 1");
  if (pack->e_o_s) {
    stream->eos = 1;
    pack->e_o_s = 1;
  }

  if (((double)pack->granulepos * 1000.0 / (double)stream->sample_rate) >
      highest_ts)
    highest_ts = (double)pack->granulepos * 1000.0 /
                  (double)stream->sample_rate;

  switch (stream->stype) {
    case 'v':
      if (!extraction_requested(xvideo, stream->sno, NOVIDEO))
        return;
      break;
    case 'a':
      if (!extraction_requested(xaudio, stream->sno, NOAUDIO))
        return;
      break;
    case 't':
      if (!extraction_requested(xtext, stream->sno, NOTEXT))
        return;
      break;
  }

  hdrlen = (*pack->packet & OGM_PACKET_LEN_BITS01) >> 6;
  hdrlen |= (*pack->packet & OGM_PACKET_LEN_BITS2) << 1;
  for (i = 0, lenbytes = 0; i < hdrlen; i++) {
    lenbytes = lenbytes << 8;
    lenbytes += *((unsigned char *)pack->packet + hdrlen - i);
  }

  switch (stream->stype) {
    case 'v':
      if (((*pack->packet & 3) == OGM_PACKET_TYPE_HEADER) ||
          ((*pack->packet & 3) == OGM_PACKET_TYPE_COMMENT))
        return;
      i = tc_pwrite(stream->fd, (char *)&pack->packet[hdrlen + 1],
                    pack->bytes - 1 - hdrlen);
      if (i != pack->bytes - 1 - hdrlen) {
        tc_log_error(__FILE__, "error while writing data");
        import_exit(1); /* XXX ugly */
      }
      if (verbose_flag & TC_DEBUG)
        tc_log_msg(__FILE__, "x/v%d: %d written", stream->sno, i);
      break;
    case 't':
      if (((*pack->packet & 3) == OGM_PACKET_TYPE_HEADER) ||
          ((*pack->packet & 3) == OGM_PACKET_TYPE_COMMENT))
        return;

      if (xraw) {
        i = tc_pwrite(stream->fd, (char *)&pack->packet[hdrlen + 1],
                      pack->bytes - 1 - hdrlen);
        if (i != pack->bytes - 1 - hdrlen) {
          tc_log_error(__FILE__, "error while writing data");
          import_exit(1); /* XXX ugly */
        }
        if (verbose_flag & TC_DEBUG)
          tc_log_msg(__FILE__, "x/t%d: %d written", stream->sno, i);
        return;
      }

      sub = (char *)&pack->packet[hdrlen + 1];
      if ((strlen(sub) > 1) || (*sub != ' ')) {
        sst = (pack->granulepos / stream->sample_rate) * 1000;
        pgp = sst + lenbytes;
        tc_snprintf(out,sizeof(out), "%d\r\n%02d:%02d:%02d,%03d --> " \
		    "%02d:%02d:%02d,%03d\r\n", stream->subnr + 1,
		    (int)(sst / 3600000),
		    (int)(sst / 60000) % 60,
		    (int)(sst / 1000) % 60,
		    (int)(sst % 1000),
		    (int)(pgp / 3600000),
		    (int)(pgp / 60000) % 60,
		    (int)(pgp / 1000) % 60,
		    (int)(pgp % 1000));
        i = tc_pwrite(stream->fd, out, strlen(out));
        if (i != strlen(out)) {
          tc_log_error(__FILE__, "error while writing data");
          import_exit(1); /* XXX ugly */
        }
        end = strlen(sub) - 1;
        while ((end >= 0) && ((sub[end] == '\n') || (sub[end] == '\r'))) {
          sub[end] = 0;
          end--;
        }
        w = tc_pwrite(stream->fd, sub, strlen(sub));
        if (i == strlen(sub)) {
            i += w;
        } else {
            tc_log_error(__FILE__, "error while writing data");
            import_exit(1);
        }
        w = tc_pwrite(stream->fd, "\r\n\r\n", 4);
        if (w == 4) {
            i += w;
        } else {
            tc_log_error(__FILE__, "error while writing data");
            import_exit(1);
        }
        stream->subnr++;
        if (verbose_flag & TC_DEBUG)
          tc_log_msg(__FILE__, "x/t%d: %d written", stream->sno, i);
      }
      break;
    case 'a':
      switch (stream->acodec) {
        case ACVORBIS:
          if (xraw) {
            if (stream->packetno == 0) {
              i = tc_pwrite(stream->fd, (char *)pack->packet, pack->bytes);
              if (i != pack->bytes) {
                tc_log_error(__FILE__, "error while writing data");
                import_exit(1); /* XXX ugly */
              }
            } else {
              i = tc_pwrite(stream->fd, (char *)&pack->packet[1],
                        pack->bytes - 1);
              if (i != pack->bytes - 1) {
                tc_log_error(__FILE__, "error while writing data");
                import_exit(1); /* XXX ugly */
              }
            }
            if (verbose_flag & TC_DEBUG)
              tc_log_msg(__FILE__, "x/a%d: %d written", stream->sno, i);
            return;
          }
          stream->max_granulepos = (pack->granulepos > stream->max_granulepos ?
                                    pack->granulepos : stream->max_granulepos);
          if ((stream->packetno == 0) || (stream->packetno == 2)) {
            ogg_stream_packetin(&stream->outstate, pack);
            flush_pages(stream, pack);
          } else {
            ogg_stream_packetin(&stream->outstate, pack);
            write_pages(stream, pack);
          }
          stream->packetno++;
          break;
        default:
          if (((*pack->packet & 3) == OGM_PACKET_TYPE_HEADER) ||
              ((*pack->packet & 3) == OGM_PACKET_TYPE_COMMENT))
            return;

          i = tc_pwrite(stream->fd, pack->packet + 1 + hdrlen,
                        pack->bytes - 1 - hdrlen);
          if (i != pack->bytes - 1 - hdrlen) {
            tc_log_error(__FILE__, "error while writing data");
            import_exit(1); /* XXX ugly */
          }
          stream->bwritten += i;
          if (verbose_flag & TC_DEBUG)
            tc_log_msg(__FILE__, "x/a%d: %d written", stream->sno, i);
          break;
      }
    break;
  }
}

static void process_ogm(int fdin, int fdout)
{
  ogg_sync_state    sync;
  ogg_page          page;
  ogg_packet        pack;
  vorbis_info       vi;
  vorbis_comment    vc;
  char             *buf, *new_name;
  int               nread, np, sno;
  int               endofstream = 0;
  stream_t         *stream = NULL;

  ogg_sync_init(&sync);
  while (1) {
    np = ogg_sync_pageseek(&sync, &page);
    if (np < 0) {
      tc_log_error(__FILE__, "ogg_sync_pageseek failed");
      return;
    }
    if (np == 0) {
      buf = ogg_sync_buffer(&sync, BLOCK_SIZE);
      if (!buf) {
        tc_log_error(__FILE__, "ogg_sync_buffer failed");
        return;
      }
      if ((nread = read(fdin, buf, BLOCK_SIZE)) <= 0) {
        if (verbose_flag & TC_INFO)
          tc_log_info(__FILE__, "end of stream 1");
        return;
      }
      ogg_sync_wrote(&sync, nread);
      continue;
    }

    if (!ogg_page_bos(&page)) {
      break;
    } else {
      ogg_stream_state sstate;
      sno = ogg_page_serialno(&page);
      if (ogg_stream_init(&sstate, sno)) {
        tc_log_error(__FILE__, "ogg_stream_init failed");
        return;
      }
      ogg_stream_pagein(&sstate, &page);
      ogg_stream_packetout(&sstate, &pack);

      if ((pack.bytes >= 7) && ! strncmp(&pack.packet[1], "vorbis", 6)) {

        stream = tc_malloc(sizeof(stream_t));
        if (stream == NULL) {
          tc_log_error(__FILE__, "malloc failed.");
          exit(1);
        }

        memset(stream, 0, sizeof(stream_t));
        if (verbose_flag & TC_INFO) {
          vorbis_info_init(&vi);
          vorbis_comment_init(&vc);
          if (vorbis_synthesis_headerin(&vi, &vc, &pack) >= 0) {
            tc_log_info(__FILE__, "(a%d/%d) Vorbis audio (channels %d "
                        "rate %ld)", nastreams + 1, numstreams + 1,
                        vi.channels, vi.rate);
            stream->sample_rate = vi.rate;
          } else
            tc_log_warn(__FILE__, "(a%d/%d) Vorbis audio stream indicated "
                        "but no Vorbis stream header found.",
                        nastreams + 1, numstreams + 1);
        }
        stream->serial = sno;
        stream->acodec = ACVORBIS;
        stream->sample_rate = -1;
        stream->sno = nastreams + 1;
        stream->stype = 'a';
        ac_memcpy(&stream->instate, &sstate, sizeof(sstate));
        if (extraction_requested(xaudio, nastreams + 1, NOAUDIO)) {
	  stream->fd = fdout;
          if (stream->fd == -1) {
            tc_log_error(__FILE__, "Failed to create \"%s\" (%d, %s).",
                         "new_name", errno, strerror(errno));
            exit(1);
          }
          if (!xraw)
            ogg_stream_init(&stream->outstate, rand());
          if (verbose_flag & TC_INFO)
            tc_log_info(__FILE__, "Extracting a%d to \"%s\".",
                        nastreams + 1, "new_name");
          do
            handle_packet(stream, &pack, &page);
          while (ogg_stream_packetout(&stream->instate, &pack) == 1);
        }
        add_stream(stream);
        nastreams++;
        numstreams++;
      } else if ((pack.bytes >= 142) &&
                 !strncmp(&pack.packet[1],"Direct Show Samples embedded in Ogg",
                          35) ) {
        if ((*(int32_t*)(pack.packet+96) == 0x05589f80) &&
            (pack.bytes >= 184)) {
          tc_log_warn(__FILE__, "(v%d/%d) Found old video header. Not "
                      "supported.", nvstreams + 1, numstreams + 1);
        } else if (*(int32_t*)pack.packet+96 == 0x05589F81) {
          tc_log_warn(__FILE__, "(a%d/%d) Found old audio header. Not "
                      "supported.", nastreams + 1, numstreams + 1);
        } else {
          if (verbose_flag & TC_INFO)
            tc_log_warn(__FILE__, "OGG stream %d has an old header with an "
                        "unknown type.", numstreams + 1);
        }
      }  else if (((*pack.packet & OGM_PACKET_TYPE_BITS ) == OGM_PACKET_TYPE_HEADER) &&
	          (pack.bytes >= (int)(sizeof(ogm_stream_header) + 1 - sizeof(int)))) {
        ogm_stream_header *sth = (ogm_stream_header *)(pack.packet + 1);
        if (!strncmp(sth->streamtype, "video", 5)) {
          unsigned long codec;
          char ccodec[5];
          strncpy(ccodec, sth->subtype, 4);
          ccodec[4] = 0;
          codec = (sth->subtype[0] << 24) +
            (sth->subtype[1] << 16) + (sth->subtype[2] << 8) + sth->subtype[3];
          if (verbose_flag & TC_INFO)
            tc_log_info(__FILE__, "(v%d/%d) fps: %.3f width height: %dx%d "
                        "codec: %p (%s)", nvstreams + 1, numstreams + 1,
                        (double)10000000 / (double)sth->time_unit,
                        sth->sh.video.width, sth->sh.video.height,
                        (void *)codec, ccodec);
          stream = tc_malloc(sizeof(stream_t));
          if (stream == NULL) {
            tc_log_error(__FILE__, "malloc failed.");
            exit(1);
          }
          stream->stype = 'v';
          stream->serial = sno;
          stream->sample_rate = (double)10000000 / (double)sth->time_unit;
          stream->sno = nvstreams + 1;
          ac_memcpy(&stream->instate, &sstate, sizeof(sstate));
          if (extraction_requested(xvideo, nvstreams + 1, NOVIDEO)) {
	    stream->fd = fdout;

            if (verbose_flag & TC_INFO)
              tc_log_info(__FILE__, "Extracting v%d to \"%s\".",
                          nvstreams + 1, "new_name");
            do {
              handle_packet(stream, &pack, &page);
	    } while (ogg_stream_packetout(&stream->instate, &pack) == 1);
          }
          add_stream(stream);
          nvstreams++;
          numstreams++;
        } else if (!strncmp(sth->streamtype, "audio", 5)) {
          int codec;
          char buf[5];
          ac_memcpy(buf, sth->subtype, 4);
          buf[4] = 0;
          codec = strtoul(buf, NULL, 16);
          if (verbose_flag & TC_INFO) {
            tc_log_info(__FILE__, "(a%d/%d) codec: %d (0x%04x) (%s), bits per "
                        "sample: %d channels: %hd  samples per second: %lld "
                        " avgbytespersec: %hd blockalign: %d",
                        nastreams + 1, numstreams + 1, codec, codec,
                        codec == ACPCM ? "PCM" : codec == 55 ? "MP3" :
                        codec == ACMP3 ? "MP3" :
                        codec == ACAC3 ? "AC3" : "unknown",
                        sth->bits_per_sample, sth->sh.audio.channels,
                        (long long)sth->samples_per_unit,
                        sth->sh.audio.avgbytespersec, sth->sh.audio.blockalign);
          }
          stream = tc_malloc(sizeof(stream_t));
          if (stream == NULL) {
            tc_log_error(__FILE__, "malloc failed.");
            exit(1);
          }
          stream->sno = nastreams + 1;
          stream->stype = 'a';
          stream->sample_rate = sth->samples_per_unit *
                                sth->sh.audio.channels;
          stream->serial = sno;
          stream->acodec = codec;
          ac_memcpy(&stream->instate, &sstate, sizeof(sstate));
          if (extraction_requested(xaudio, nastreams + 1, NOAUDIO)) {

	    /*
                      codec == ACPCM ? "wav" :
                      codec == ACMP3 ? "mp3" :
                      codec == ACAC3 ? "ac3" : "audio");
		      */
            stream->fd = fdout;
            if (verbose_flag & TC_INFO)
              tc_log_info(__FILE__, "Extracting a%d to \"%s\".",
                          nastreams + 1, "new_name");
            do {
              handle_packet(stream, &pack, &page);
	    } while (ogg_stream_packetout(&stream->instate, &pack) == 1);
          }
          add_stream(stream);
          nastreams++;
          numstreams++;
        } else if (!strncmp(sth->streamtype, "text", 4)) {
          if (verbose_flag & TC_INFO)
            tc_log_info(__FILE__, "(t%d/%d) text/subtitle stream",
                        ntstreams + 1, numstreams + 1);
          stream = tc_malloc(sizeof(stream_t));
          if (stream == NULL) {
            tc_log_error(__FILE__, "malloc failed.");
            exit(1);
          }
          stream->sno = ntstreams + 1;
          stream->stype = 't';
          stream->sample_rate = (double)10000000 / (double)sth->time_unit;
          stream->serial = sno;
          ac_memcpy(&stream->instate, &sstate, sizeof(sstate));
          if (extraction_requested(xtext, ntstreams + 1, NOTEXT)) {
            new_name = tc_malloc(strlen(basename) + 20);
            if (!new_name) {
              tc_log_error(__FILE__, "Failed to allocate %d bytes.",
                           (int)strlen(basename) + 20);
              exit(1);
            }
            if (!xraw)
              tc_snprintf(new_name, strlen(basename) + 20, "%s-t%d.srt", basename, ntstreams + 1);
            else
              tc_snprintf(new_name, strlen(basename) + 20, "%s-t%d.raw", basename, ntstreams + 1);
            //stream->fd = open(new_name, O_WRONLY | O_CREAT | O_TRUNC,
            //                S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
            stream->fd = fdout;
            if (stream->fd == -1) {
              tc_log_error(__FILE__, "Failed to create \"%s\" (%d, %s).",
                           new_name, errno, strerror(errno));
              exit(1);
            }
            if (verbose_flag & TC_INFO)
              tc_log_info(__FILE__, "Extracting t%d to \"%s\".",
                          ntstreams + 1, new_name);
            do
              handle_packet(stream, &pack, &page);
            while (ogg_stream_packetout(&stream->instate, &pack) == 1);
          }
          add_stream(stream);
          ntstreams++;
          numstreams++;
        } else {
          tc_log_warn(__FILE__, "(%d) found new header of unknown/"
                      "unsupported type\n", numstreams + 1);
        }

      } else {
	tc_log_warn(__FILE__, "OGG stream %d is of an unknown type "
                    "(bad header?)\n", numstreams + 1);
      }
    }
  }

  endofstream = 0;
  while (!endofstream) {
    sno = ogg_page_serialno(&page);
    stream = find_stream(sno);
    if (stream == NULL) {
      if (verbose_flag & TC_DEBUG)
        tc_log_warn(__FILE__, "Encountered packet for an unknown serial "
                    "%d !?\n", sno);
    } else {
      if (verbose_flag & TC_DEBUG)
        tc_log_msg(__FILE__, "%c%d: NEW PAGE", stream->stype, stream->sno);

      ogg_stream_pagein(&stream->instate, &page);
      while (ogg_stream_packetout(&stream->instate, &pack) == 1)
        handle_packet(stream, &pack, &page);
    }

    while (ogg_sync_pageseek(&sync, &page) <= 0) {
      buf = ogg_sync_buffer(&sync, BLOCK_SIZE);
      nread = read(fdin, buf, BLOCK_SIZE);
      if (nread <= 0) {
        stream = first;
        while (stream != NULL) {
          switch (stream->stype) {
            case 'v':
	      close(stream->fd);
              break;
            case 't':
              if (stream->fd > 0)
                close(stream->fd);
              break;
            case 'a':
              if ((stream->fd > 0) && !xraw) {
                switch (stream->acodec) {
                  case ACVORBIS:
                    if (!stream->eos) {
                      pack.b_o_s = 0;
                      pack.e_o_s = 1;
                      pack.packet = NULL;
                      pack.bytes = 0;
                      pack.granulepos = stream->max_granulepos;
                      pack.packetno = stream->packetno;
                      ogg_stream_packetin(&stream->outstate, &pack);
                    }
                    flush_pages(stream, &pack);
                    ogg_stream_clear(&stream->outstate);
                    break;
                }
                close(stream->fd);
              } else if (stream->fd > 0)
                close(stream->fd);
              break;
          }
          stream = stream->next;
        }
        if (verbose_flag & TC_INFO)
          tc_log_info(__FILE__, "end of stream");
        endofstream = 1;
        break;
      } else
        ogg_sync_wrote(&sync, nread);
    }
  }
}
#endif /* have OGG */

void extract_ogm (info_t *ipipe)
{
  // track

  no[NOTEXT]  = 1;
  no[NOAUDIO] = 0;
  no[NOVIDEO] = 0;
  xraw = 1;

  xvideo = tc_zalloc (16); xaudio = tc_zalloc (16); xtext = tc_zalloc (16);

  verbose_flag = ipipe->verbose;

  if (ipipe->select == TC_VIDEO) {

    no[NOAUDIO] = 1;
    xvideo[0] = (unsigned char)(ipipe->track+1);

  }

  if (ipipe->select == TC_AUDIO) {

    no[NOVIDEO] = 1;
    xaudio[0] = (unsigned char)(ipipe->track+1);

    // we need !xraw because no tool seems to be able to handle
    // raw vorbis streams -- tibit

    if (ipipe->codec == TC_CODEC_VORBIS) {
      xraw = 0;
    }

  }

#if (HAVE_OGG && HAVE_VORBIS)
  process_ogm(ipipe->fd_in, ipipe->fd_out);
#else
  tc_log_error(__FILE__, "No support for Ogg/Vorbis compiled in");
  import_exit(1);
#endif
}



/* vim: sw=2
 */
