/* ogg.c
 * Copyright (C) 2004, 2005 Sylvain Cresto <scresto@gmail.com>
 *
 * This file is part of graveman!
 *
 * graveman! 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, or
 * (at your option) any later version.
 * 
 * graveman! 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 program; see the file COPYING. If not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
 * MA 02111-1307, USA. 
 * 
 * URL: http://www.nongnu.org/graveman/
 *
 */

#include "graveman.h"

#ifdef ENABLE_OGG
#include <vorbis/codec.h>

/* ce code est basé sur ogginfo2.c, livré avec les vorbis-tools-1.0
 * en telechargement sur http://www.vorbis.com/download.psp */



/* Ogginfo
 *
 * A tool to describe ogg file contents and metadata.
 *
 * Copyright 2002 Michael Smith <msmith@layrinth.net.au>
 * Licensed under the GNU GPL, distributed with this program.
 */
#define OGG_TITLE "TITLE"
#define OGG_ALBUM "ALBUM"
#define OGG_ARTIST "ARTIST"

#define CHUNK 4500

/* TODO:
 *
 * - detect decreasing granulepos
 * - detect violations of muxing constraints
 * - better EOS detection (when EOS not explicitly set)
 * - detect granulepos 'gaps' (possibly vorbis-specific).
 * - check for serial number == (unsigned)-1 (will break some tools?)
 * - more options (e.g. less or more verbose)
 */

typedef struct _stream_processor {
  void (*process_page)(struct _stream_processor *, ogg_page *);
  void (*process_end)(struct _stream_processor *);
  gint isillegal;
  gint shownillegal;
  gint isnew;
  glong seqno;
  gint lostseq;

  gint start;
  gint end;

  gint num;
  gchar *type;

  ogg_uint32_t serial; /* must be 32 bit unsigned */
  ogg_stream_state os;
  void *data;
} stream_processor;

typedef struct {
  stream_processor *streams;
  gint allocated;
  gint used;

  gint in_headers;
} stream_set;

typedef struct {
  glong bytes;
  vorbis_comment vc;
  vorbis_info vi;
  ogg_int64_t lastgranulepos;

  gint doneheaders;

  gchar *title, *artist, *album; /* les informations qui nous interessent */
/*  glong minutes, seconds; */
  guint32 length;
} misc_vorbis_info;

static stream_set *create_stream_set(void) {
  stream_set *set = g_malloc0(sizeof(stream_set));

  set->streams = g_new0(stream_processor, 5);
  set->allocated = 5;
  set->used = 0;

  return set;
}

static void vorbis_process(stream_processor *stream, ogg_page *page )
{
  ogg_packet packet;
  misc_vorbis_info *inf = stream->data;
  gint i, header=0;

  ogg_stream_pagein(&stream->os, page);

  while(ogg_stream_packetout(&stream->os, &packet) > 0) {
    if(inf->doneheaders < 3) {
      if(vorbis_synthesis_headerin(&inf->vi, &inf->vc, &packet) < 0) {
        g_warning(_("Warning: Could not decode vorbis header packet - invalid vorbis stream (%d)"), stream->num);
        continue;
      }
      header = 1;
      inf->doneheaders++;

      if(inf->doneheaders == 3) {
        for(i=0; i < inf->vc.comments; i++) {
          gchar *sep = strchr(inf->vc.user_comments[i], '=');

          /* extraction des commentaires */
          if(sep == NULL) continue;

          *(sep++) = 0;
          if (!g_ascii_strcasecmp(inf->vc.user_comments[i], OGG_TITLE)) {
            inf->title = sc_realloc_cat(" ", inf->title, sep);
          } else if (!g_ascii_strcasecmp(inf->vc.user_comments[i], OGG_ARTIST)) {
            inf->artist = sc_realloc_cat(" ", inf->artist, sep);
          } else if (!g_ascii_strcasecmp(inf->vc.user_comments[i], OGG_ALBUM)) {
            inf->album = sc_realloc_cat(" ", inf->album, sep);
          }
        }
      }
    }
  }

  if(!header) {
    ogg_int64_t gp = ogg_page_granulepos(page);
    if(gp > 0) {
      inf->lastgranulepos = gp;
    }
    inf->bytes += page->header_len + page->body_len;
  }
}

static void vorbis_end(stream_processor *stream) 
{
  misc_vorbis_info *inf = stream->data;

  inf->length = (guint32)inf->lastgranulepos / inf->vi.rate;

  vorbis_comment_clear(&inf->vc);
  vorbis_info_clear(&inf->vi);
}

static void process_null(stream_processor *stream, ogg_page *page)
{
  /* This is for invalid streams. */
}

static void free_stream_set(stream_set *set)
{
  gint i;
  for(i=0; i < set->used; i++) {
    if(!set->streams[i].end) {
      g_warning(_("Warning: EOS not set on stream %d"), set->streams[i].num);
      if(set->streams[i].process_end)
        set->streams[i].process_end(&set->streams[i]);
    }
    ogg_stream_clear(&set->streams[i].os);
  }

  free(set->streams);
  free(set);
}

static int streams_open(stream_set *set)
{
  gint i;
  gint res=0;
  for(i=0; i < set->used; i++) {
    if(!set->streams[i].end)
    res++;
  }

  return res;
}

static void null_start(stream_processor *stream)
{
  stream->process_end = NULL;
  stream->type = "invalid";
  stream->process_page = process_null;
}

static void vorbis_start(stream_processor *stream, misc_vorbis_info *inf)
{
  stream->type = "vorbis";
  stream->process_page = vorbis_process;
  stream->process_end = vorbis_end;

  stream->data = inf;

  inf = stream->data;

  vorbis_comment_init(&inf->vc);
  vorbis_info_init(&inf->vi);
}

static stream_processor *find_stream_processor(stream_set *set, ogg_page *page, misc_vorbis_info *inf)
{
  ogg_uint32_t serial = ogg_page_serialno(page);
  gint i, found = 0;
  gint invalid = 0;
  stream_processor *stream;

  for(i=0; i < set->used; i++) {
    if(serial == set->streams[i].serial) {
      /* We have a match! */
      found = 1;
      stream = &(set->streams[i]);

      set->in_headers = 0;
      /* if we have detected EOS, then this can't occur here. */
      if(stream->end) {
        stream->isillegal = 1;
        return stream;
      }

      stream->isnew = 0;
      stream->start = ogg_page_bos(page);
      stream->end = ogg_page_eos(page);
      stream->serial = serial;
      return stream;
    }
  }

  /* If there are streams open, and we've reached the end of the
   * headers, then we can't be starting a new stream.
   * XXX: might this sometimes catch ok streams if EOS flag is missing,
   * but the stream is otherwise ok?
   */
  if(streams_open(set) && !set->in_headers)
    invalid = 1;

  set->in_headers = 1;

  if(set->allocated < set->used) stream = &set->streams[set->used];
  else {
    set->allocated += 5;
    set->streams = realloc(set->streams, sizeof(stream_processor)* set->allocated);
    stream = &set->streams[set->used];
  }
  set->used++;
  stream->num = set->used; /* We count from 1 */

  stream->isnew = 1;
  stream->isillegal = invalid;

  {
    gint res;
    ogg_packet packet;

    /* We end up processing the header page twice, but that's ok. */
    ogg_stream_init(&stream->os, serial);
    ogg_stream_pagein(&stream->os, page);
    res = ogg_stream_packetout(&stream->os, &packet);
    if(res > 0 && packet.bytes >= 7 && memcmp(packet.packet, "\001vorbis", 7)==0) {
      vorbis_start(stream, inf);
    } else {
      g_warning(_("Warning: no vorbis"));
      null_start(stream);
    }

    res = ogg_stream_packetout(&stream->os, &packet);
    if(res > 0) {
      g_warning(_("Warning: Invalid header page in stream %d, contains multiple packets"), stream->num);
    }

    /* re-init, ready for processing */
    ogg_stream_clear(&stream->os);
    ogg_stream_init(&stream->os, serial);
  }

  stream->start = ogg_page_bos(page);
  stream->end = ogg_page_eos(page);
  stream->serial = serial;

  return stream;
}

static gint get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page, 
        ogg_int64_t *written)
{
  gint ret;
  gchar *buffer;
  gint bytes;

  while((ret = ogg_sync_pageout(sync, page)) <= 0) {
    buffer = ogg_sync_buffer(sync, CHUNK);
    bytes = fread(buffer, 1, CHUNK, f);
    if(bytes <= 0) {
      ogg_sync_wrote(sync, 0);
      return 0;
    }
    ogg_sync_wrote(sync, bytes);
    *written += bytes;
  }

  return 1;
}

gboolean getOggInfo(gchar *Apath, gchar **Atitle, gchar **Aalbum, gchar **Aartist, guint32 *Alength, GError **Aerror) {
  FILE *Lfile = fopen(Apath, "rb");
  ogg_sync_state sync;
  ogg_page page;
  stream_set *processors = create_stream_set();
  gint gotpage = 0;
  ogg_int64_t written = 0;
  misc_vorbis_info inf;

  *(Atitle) = *(Aalbum) = *(Aartist) = NULL;
  *(Alength) = 0;
    
  bzero(&inf, sizeof(misc_vorbis_info));

  if(!Lfile) {
     g_set_error(Aerror, G_FILE_ERROR, g_file_error_from_errno(errno), "%s:%s\n%s",
         _("Cannot read file"), Apath, strerror(errno));
    return FALSE;
  }

  ogg_sync_init(&sync);

  while(get_next_page(Lfile, &sync, &page, &written)) {
    stream_processor *p = find_stream_processor(processors, &page, &inf);
    gotpage = 1;
    if(!p) {
      g_set_error(Aerror, GRAVEMAN_ERROR, _ERR_INAPPROPRIATE_DATA, _("%s is not a valid .ogg file !"), Apath);
      return FALSE;
    }

    if(p->isillegal && !p->shownillegal) {
      g_warning(_("Warning: illegally placed page(s) for logical stream %d\n"
                  "This indicates a corrupt ogg file."), p->num);
      p->shownillegal = 1;
      continue;
    }

    if(p->seqno++ != ogg_page_pageno(&page)) {
      if(!p->lostseq) 
        g_warning(_("Warning: sequence number gap in stream %d. Got "
                    "page %ld when expecting page %ld. Indicates missing data."),
                    p->num, ogg_page_pageno(&page), p->seqno - 1);
      p->seqno = ogg_page_pageno(&page);
      p->lostseq = 1;
    } else p->lostseq = 0;

    if(!p->isillegal) {
      p->process_page(p, &page);
      if(p->end) {
        if(p->process_end) p->process_end(p);
        p->isillegal = 1;
      }
    }
  }

  free_stream_set(processors);

  ogg_sync_clear(&sync);

  fclose(Lfile);

  if (gotpage) {
    *Atitle = g_strdup(inf.title ? inf.title : "");
    *Aalbum = g_strdup(inf.album ? inf.album : "");
    *Aartist = g_strdup(inf.artist ? inf.artist : "");
/*    *Alength = g_strdup_printf("%02ld:%02ld", inf.minutes, inf.seconds); */
    *(Alength) = inf.length;
  } else {
    g_set_error(Aerror, GRAVEMAN_ERROR, _ERR_INAPPROPRIATE_DATA, _("%s is not a valid .ogg file !"), Apath);
  }

  if (inf.title) g_free(inf.title);
  if (inf.album) g_free(inf.album);
  if (inf.artist) g_free(inf.artist);

  return gotpage ? TRUE : FALSE;
}

#endif  /* ifdef ENABLE_OGG */

/*
 * vim:et:ts=8:sts=2:sw=2
 */
