/********************************************************************
 *                                                                  *
 * THIS FILE IS PART OF THE OggTheora SOFTWARE CODEC SOURCE CODE.   *
 * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS     *
 * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE *
 * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING.       *
 *                                                                  *
 * THE Theora SOURCE CODE IS COPYRIGHT (C) 2002-2009                *
 * by the Xiph.Org Foundation and contributors http://www.xiph.org/ *
 *                                                                  *
 ********************************************************************

  function: example SDL player application; plays Ogg Theora files (with
            optional Vorbis audio second stream)
  last mod: $Id: player_example.c 16551 2009-09-09 17:53:13Z gmaxwell $

 ********************************************************************/

/* far more complex than most Ogg 'example' programs.  The complexity
   of maintaining A/V sync is pretty much unavoidable.  It's necessary
   to actually have audio/video playback to make the hard audio clock
   sync actually work.  If there's audio playback, there might as well
   be simple video playback as well...

   A simple 'demux and write back streams' would have been easier,
   it's true. */

#if !defined(_GNU_SOURCE)
#define _GNU_SOURCE
#endif
#if !defined(_LARGEFILE_SOURCE)
#define _LARGEFILE_SOURCE
#endif
#if !defined(_LARGEFILE64_SOURCE)
#define _LARGEFILE64_SOURCE
#endif
#if !defined(_FILE_OFFSET_BITS)
#define _FILE_OFFSET_BITS 64
#endif

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#ifndef _REENTRANT
# define _REENTRANT
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>
#include <signal.h>
#include "theora/theoradec.h"
#include "vorbis/codec.h"
#include <SDL.h>
#include <SDL/SDL_audio.h>
//#include <SDL/SDL_mixer.h>
/* Good default values for a PC soundcard */
#define MIX_DEFAULT_FREQUENCY	22050
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
#define MIX_DEFAULT_FORMAT	AUDIO_S16LSB
#else
#define MIX_DEFAULT_FORMAT	AUDIO_S16MSB
#endif
#define MIX_DEFAULT_CHANNELS	2
#define MIX_MAX_VOLUME		128	/* Volume of a chunk */

/* Helper; just grab some more compressed bitstream and sync it for
   page extraction */
int buffer_data(FILE *in,ogg_sync_state *oy){
  char *buffer=ogg_sync_buffer(oy,4096);
  int bytes=fread(buffer,1,4096,in);
  ogg_sync_wrote(oy,bytes);
  return(bytes);
}

/* never forget that globals are a one-way ticket to Hell */
/* Ogg and codec state for demux/decode */
ogg_sync_state   oy;
ogg_page         og;
ogg_stream_state vo;
ogg_stream_state to;
th_info      ti;
th_comment   tc;
th_dec_ctx       *td;
th_setup_info    *ts;
vorbis_info      vi;
vorbis_dsp_state vd;
vorbis_block     vb;
vorbis_comment   vc;
th_pixel_fmt     px_fmt;

int              theora_p=0;
int              vorbis_p=0;
int              stateflag=0;

/* SDL Video playback structures */
SDL_Surface *screen;
SDL_Overlay *yuv_overlay;
SDL_Rect rect;

/* single frame video buffering */
ogg_int64_t  videobuf_granulepos=-1;

/* single audio fragment audio buffering */
int          audiobuf_total=0;
int          audiobuf_available = 0;
int          audiofd_fragsize = 2048;//4096;
int          volume = SDL_MIX_MAXVOLUME;
SDL_AudioCVT cvt;

void audio_write_nonblocking( void *userdata, unsigned char *stream, int len );

/* clean quit on Ctrl-C for SDL and thread shutdown as per SDL example
   (we don't use any threads, but libSDL does) */
int got_sigint=0;
static void sigint_handler (int signal) {
  got_sigint = 1;
}

/* helper: push a page into the appropriate steam */
/* this can be done blindly; a stream won't accept a page
                that doesn't belong to it */
static int queue_page(ogg_page *page){
  if(theora_p)ogg_stream_pagein(&to,page);
  if(vorbis_p)ogg_stream_pagein(&vo,page);
  return 0;
}

static void open_audio(FILE* infile){
  SDL_AudioSpec fmt;
  SDL_AudioSpec got;
  fmt.freq      = MIX_DEFAULT_FREQUENCY;
  fmt.format    = MIX_DEFAULT_FORMAT;
  fmt.channels  = MIX_DEFAULT_CHANNELS;
  fmt.samples   = audiofd_fragsize;
  fmt.callback  = audio_write_nonblocking; // !!!
  fmt.userdata  = infile; // !!!
  if ( SDL_OpenAudio( &fmt, &got ) < 0 ) {
    fprintf(stderr,"Could not open audio device.\n");
    exit(1);
  }
  printf( "audio hw: format[%x] freq[%d] chan[%d]\n", got.format, got.freq, got.channels );
  SDL_BuildAudioCVT(&cvt, AUDIO_S16, vi.channels, vi.rate,
                          got.format,got.channels,got.freq);
  cvt.buf = (Uint8*)malloc(audiofd_fragsize);
}

/* decode audio when SDL_audio wants to play something
 * instead of using a rube goldberg timer handshaking system */
void audio_write_nonblocking( void *userdata, unsigned char *stream, int len )
{
  while ( vorbis_p && len > 0 ) {
    int audiobuf_fill;
    if ( ! audiobuf_available ) {
      ogg_packet op;
      int i,j;
      int ret;
      float **pcm;
      ogg_int16_t *p;
      int size;
      /* no pending audio; is there a pending packet to decode? */
      while(ogg_stream_packetout(&vo,&op)<1){
        while(ogg_sync_pageout(&oy,&og)<1){
          /* no data.  Grab another page */
          if ( feof(userdata) || ! buffer_data(userdata,&oy) ) {
            got_sigint = 1;
            return;
          }
        }
        queue_page(&og);
      }
      if(vorbis_synthesis(&vb,&op)==0) /* test for success! */
        vorbis_synthesis_blockin(&vd,&vb);
      else
        return;

      /* if there's pending, decoded audio, grab it */
      if((ret=vorbis_synthesis_pcmout(&vd,&pcm))<1)
        return;
      p = (ogg_int16_t*)cvt.buf;
      size = ret*vi.channels*sizeof(ogg_int16_t);
      for(i=0;i<ret;i++) {
        for(j=0;j<vi.channels;j++){
          int val=rint(pcm[j][i]*32767.f);
          *p++=0xffff&((ogg_int16_t)val);
        }
      }
      vorbis_synthesis_read(&vd,i);
      // prep for SDL_audio
      if ( cvt.needed ) {
        cvt.len = size;
        SDL_ConvertAudio(&cvt);
      } else {
        cvt.len_cvt = size;
      }
      audiobuf_available = cvt.len_cvt;
    }
    audiobuf_fill = len;
    if ( audiobuf_fill > audiobuf_available ) {
      audiobuf_fill = audiobuf_available;
    }
    // play it on SDL_audio
    if ( volume == MIX_MAX_VOLUME ) {
      memcpy(stream, cvt.buf, audiobuf_fill);
    } else {
      SDL_MixAudio(stream, cvt.buf, audiobuf_fill, volume);
    }
    len -= audiobuf_fill;
    stream += audiobuf_fill;
    audiobuf_total += audiobuf_fill;
    audiobuf_available -= audiobuf_fill;
    if ( audiobuf_available ) {
      // if here, len should be 0
      if ( len )
        fprintf(stderr, "bad math\n");
      memcpy(cvt.buf, cvt.buf+audiobuf_fill, audiobuf_available);
    }
  }
}

static void open_video(void){
  int w;
  int h;
  w=(ti.pic_x+ti.frame_width+1&~1)-(ti.pic_x&~1);
  h=(ti.pic_y+ti.frame_height+1&~1)-(ti.pic_y&~1);
  if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
    fprintf(stderr, "Unable to init SDL: %s\n", SDL_GetError());
    exit(1);
  }

  screen = SDL_SetVideoMode(w, h, 0, SDL_SWSURFACE);
  if ( screen == NULL ) {
    fprintf(stderr, "Unable to set %dx%d video: %s\n",
            w,h,SDL_GetError());
    exit(1);
  }

  if (px_fmt==TH_PF_422)
    yuv_overlay = SDL_CreateYUVOverlay(w, h,
                                     SDL_YUY2_OVERLAY,
                                     screen);
  else
    yuv_overlay = SDL_CreateYUVOverlay(w, h,
                                     SDL_YV12_OVERLAY,
                                     screen);
  
  if ( yuv_overlay == NULL ) {
    fprintf(stderr, "SDL: Couldn't create SDL_yuv_overlay: %s\n",
            SDL_GetError());
    exit(1);
  }
  rect.x = 0;
  rect.y = 0;
  rect.w = w;
  rect.h = h;

  SDL_DisplayYUVOverlay(yuv_overlay, &rect);
}

static void video_write(void){
  int i;
  th_ycbcr_buffer yuv;
  int y_offset, uv_offset;
  th_decode_ycbcr_out(td,yuv);
  /* Lock SDL_yuv_overlay */
  if ( SDL_MUSTLOCK(screen) ) {
    if ( SDL_LockSurface(screen) < 0 ) return;
  }
  if (SDL_LockYUVOverlay(yuv_overlay) < 0) return;

  /* let's draw the data on a SDL screen (*screen) */
  /* deal with border stride */
  /* reverse u and v for SDL */
  /* and crop input properly, respecting the encoded frame rect */
  /* problems may exist for odd frame rect for some encodings */

  y_offset=(ti.pic_x&~1)+yuv[0].stride*(ti.pic_y&~1);

  if (px_fmt==TH_PF_422) {
    uv_offset=(ti.pic_x/2)+(yuv[1].stride)*(ti.pic_y);
    /* SDL doesn't have a planar 4:2:2 */ 
    for(i=0;i<yuv_overlay->h;i++) {
      int j;
      char *in_y  = (char *)yuv[0].data+y_offset+yuv[0].stride*i;
      char *out = (char *)(yuv_overlay->pixels[0]+yuv_overlay->pitches[0]*i);
      for (j=0;j<yuv_overlay->w;j++)
        out[j*2] = in_y[j];
      char *in_u  = (char *)yuv[1].data+uv_offset+yuv[1].stride*i;
      char *in_v  = (char *)yuv[2].data+uv_offset+yuv[2].stride*i;
      for (j=0;j<yuv_overlay->w>>1;j++) {
        out[j*4+1] = in_u[j];
        out[j*4+3] = in_v[j];
      }
    }
  } else {
    uv_offset=(ti.pic_x/2)+(yuv[1].stride)*(ti.pic_y/2);
    for(i=0;i<yuv_overlay->h;i++)
      memcpy(yuv_overlay->pixels[0]+yuv_overlay->pitches[0]*i,
           yuv[0].data+y_offset+yuv[0].stride*i,
           yuv_overlay->w);
    for(i=0;i<yuv_overlay->h/2;i++){
      memcpy(yuv_overlay->pixels[1]+yuv_overlay->pitches[1]*i,
           yuv[2].data+uv_offset+yuv[2].stride*i,
           yuv_overlay->w/2);
      memcpy(yuv_overlay->pixels[2]+yuv_overlay->pitches[2]*i,
           yuv[1].data+uv_offset+yuv[1].stride*i,
           yuv_overlay->w/2);
    }
  }

  /* Unlock SDL_yuv_overlay */
  if ( SDL_MUSTLOCK(screen) ) {
    SDL_UnlockSurface(screen);
  }
  SDL_UnlockYUVOverlay(yuv_overlay);


  /* Show, baby, show! */
  SDL_DisplayYUVOverlay(yuv_overlay, &rect);

}
/* dump the theora (or vorbis) comment header */
static int dump_comments(th_comment *tc){
  int i, len;
  char *value;
  FILE *out=stdout;

  fprintf(out,"Encoded by %s\n",tc->vendor);
  if(tc->comments){
    fprintf(out, "theora comment header:\n");
    for(i=0;i<tc->comments;i++){
      if(tc->user_comments[i]){
        len=tc->comment_lengths[i];
        value=malloc(len+1);
        memcpy(value,tc->user_comments[i],len);
        value[len]='\0';
        fprintf(out, "\t%s\n", value);
        free(value);
      }
    }
  }
  return(0);
}

/* Report the encoder-specified colorspace for the video, if any.
   We don't actually make use of the information in this example;
   a real player should attempt to perform color correction for
   whatever display device it supports. */
static void report_colorspace(th_info *ti)
{
    switch(ti->colorspace){
      case TH_CS_UNSPECIFIED:
        /* nothing to report */
        break;;
      case TH_CS_ITU_REC_470M:
        fprintf(stderr,"  encoder specified ITU Rec 470M (NTSC) color.\n");
        break;;
      case TH_CS_ITU_REC_470BG:
        fprintf(stderr,"  encoder specified ITU Rec 470BG (PAL) color.\n");
        break;;
      default:
        fprintf(stderr,"warning: encoder specified unknown colorspace (%d).\n",
            ti->colorspace);
        break;;
    }
}

static void usage(void){
  fprintf(stderr,
          "Usage: player_example <file.ogv>\n"
          "input is read from stdin if no file is passed on the command line\n"
          "\n"
  );
}

int main(int argc,char *const *argv){

  int pp_level_max;
  int pp_level;
  int pp_inc;
  ogg_packet op;

  FILE *infile = stdin;

  int frames = 0;
  int dropped = 0;

  struct timeval timeout;
  fd_set writefs;
  fd_set empty;
  FD_ZERO(&writefs);
  FD_ZERO(&empty);

#ifdef _WIN32 /* We need to set stdin/stdout to binary mode. Damn windows. */
  /* Beware the evil ifdef. We avoid these where we can, but this one we
     cannot. Don't add any more, you'll probably go to hell if you do. */
  _setmode( _fileno( stdin ), _O_BINARY );
#endif

  /* open the input file if any */
  if(argc==2){
    infile=fopen(argv[1],"rb");
    if(infile==NULL){
      fprintf(stderr,"Unable to open '%s' for playback.\n", argv[1]);
      exit(1);
    }
  }
  if(argc>2){
      usage();
      exit(1);
  }

  /* start up Ogg stream synchronization layer */
  ogg_sync_init(&oy);

  /* init supporting Vorbis structures needed in header parsing */
  vorbis_info_init(&vi);
  vorbis_comment_init(&vc);

  /* init supporting Theora structures needed in header parsing */
  th_comment_init(&tc);
  th_info_init(&ti);

  /* Ogg file open; parse the headers */
  /* Only interested in Vorbis/Theora streams */
  while(!stateflag){
    int ret=buffer_data(infile,&oy);
    if(ret==0)break;
    while(ogg_sync_pageout(&oy,&og)>0){
      ogg_stream_state test;

      /* is this a mandated initial header? If not, stop parsing */
      if(!ogg_page_bos(&og)){
        /* don't leak the page; get it into the appropriate stream */
        queue_page(&og);
        stateflag=1;
        break;
      }

      ogg_stream_init(&test,ogg_page_serialno(&og));
      ogg_stream_pagein(&test,&og);
      ogg_stream_packetout(&test,&op);


      /* identify the codec: try theora */
      if(!theora_p && th_decode_headerin(&ti,&tc,&ts,&op)>=0){
        /* it is theora */
        memcpy(&to,&test,sizeof(test));
        theora_p=1;
      }else if(!vorbis_p && vorbis_synthesis_headerin(&vi,&vc,&op)>=0){
        /* it is vorbis */
        memcpy(&vo,&test,sizeof(test));
        vorbis_p=1;
      }else{
        /* whatever it is, we don't care about it */
        ogg_stream_clear(&test);
      }
    }
    /* fall through to non-bos page parsing */
  }

  /* we're expecting more header packets. */
  while((theora_p && theora_p<3) || (vorbis_p && vorbis_p<3)){
    int ret;

    /* look for further theora headers */
    while(theora_p && (theora_p<3) && (ret=ogg_stream_packetout(&to,&op))){
      if(ret<0){
        fprintf(stderr,"Error parsing Theora stream headers; "
         "corrupt stream?\n");
        exit(1);
      }
      if(!th_decode_headerin(&ti,&tc,&ts,&op)){
        fprintf(stderr,"Error parsing Theora stream headers; "
         "corrupt stream?\n");
        exit(1);
      }
      theora_p++;
    }

    /* look for more vorbis header packets */
    while(vorbis_p && (vorbis_p<3) && (ret=ogg_stream_packetout(&vo,&op))){
      if(ret<0){
        fprintf(stderr,"Error parsing Vorbis stream headers; corrupt stream?\n");
        exit(1);
      }
      if(vorbis_synthesis_headerin(&vi,&vc,&op)){
        fprintf(stderr,"Error parsing Vorbis stream headers; corrupt stream?\n");
        exit(1);
      }
      vorbis_p++;
      if(vorbis_p==3)break;
    }

    /* The header pages/packets will arrive before anything else we
       care about, or the stream is not obeying spec */

    if(ogg_sync_pageout(&oy,&og)>0){
      queue_page(&og); /* demux into the appropriate stream */
    }else{
      int ret=buffer_data(infile,&oy); /* someone needs more data */
      if(ret==0){
        fprintf(stderr,"End of file while searching for codec headers.\n");
        exit(1);
      }
    }
  }

  /* and now we have it all.  initialize decoders */
  if(theora_p){
    td=th_decode_alloc(&ti,ts);
    printf("Ogg logical stream %lx is Theora %dx%d %.02f fps",
           to.serialno,ti.pic_width,ti.pic_height,
           (double)ti.fps_numerator/ti.fps_denominator);
    px_fmt=ti.pixel_fmt;
    switch(ti.pixel_fmt){
      case TH_PF_420: printf(" 4:2:0 video\n"); break;
      case TH_PF_422: printf(" 4:2:2 video\n"); break;
      case TH_PF_444: printf(" 4:4:4 video\n"); break;
      case TH_PF_RSVD:
      default:
       printf(" video\n  (UNKNOWN Chroma sampling!)\n");
       break;
    }
    if(ti.pic_width!=ti.frame_width || ti.pic_height!=ti.frame_height)
      printf("  Frame content is %dx%d with offset (%d,%d).\n",
           ti.frame_width, ti.frame_height, ti.pic_x, ti.pic_y);
    report_colorspace(&ti);
    dump_comments(&tc);
    th_decode_ctl(td,TH_DECCTL_GET_PPLEVEL_MAX,&pp_level_max,
     sizeof(pp_level_max));
    pp_level=pp_level_max;
    th_decode_ctl(td,TH_DECCTL_SET_PPLEVEL,&pp_level,sizeof(pp_level));
    pp_inc=0;

    /*{
      int arg = 0xffff;
      th_decode_ctl(td,TH_DECCTL_SET_TELEMETRY_MBMODE,&arg,sizeof(arg));
      th_decode_ctl(td,TH_DECCTL_SET_TELEMETRY_MV,&arg,sizeof(arg));
      th_decode_ctl(td,TH_DECCTL_SET_TELEMETRY_QI,&arg,sizeof(arg));
      arg=10;
      th_decode_ctl(td,TH_DECCTL_SET_TELEMETRY_BITS,&arg,sizeof(arg));
    }*/
  }else{
    /* tear down the partial theora setup */
    th_info_clear(&ti);
    th_comment_clear(&tc);
  }
  
  th_setup_free(ts);
  
  if(vorbis_p){
    vorbis_synthesis_init(&vd,&vi);
    vorbis_block_init(&vd,&vb);
    fprintf(stderr,"Ogg logical stream %lx is Vorbis %d channel %ld Hz audio.\n",
            vo.serialno,vi.channels,vi.rate);
  }else{
    /* tear down the partial vorbis setup */
    vorbis_info_clear(&vi);
    vorbis_comment_clear(&vc);
  }

  /* open audio */
  if(vorbis_p)open_audio(infile);

  /* open video */
  if(theora_p)open_video();

  /* install signal handler as SDL clobbered the default */
  signal (SIGINT, sigint_handler);

  /* on to the main decode loop.  We assume in this example that audio
     and video start roughly together, and don't begin playback until
     we have a start frame for both.  This is not necessarily a valid
     assumption in Ogg A/V streams! It will always be true of the
     example_encoder (and most streams) though. */

  stateflag=0; /* playback has not begun */
  SDL_PauseAudio(0);
  while(!got_sigint){
    // audio code moved to audio_write_nonblocking() -- called by SDL_audio
    SDL_Event event;

    if(theora_p) {
      /* theora is one in, one out... */
      double audiobuf_time = (double)audiobuf_total /
                   (double)( vi.rate * vi.channels /* *sizeof(ogg_int16_t) */ );
      double videobuf_time = th_granule_time(td,videobuf_granulepos);
      if ( !vorbis_p || audiobuf_time > videobuf_time ) {
        /* no pending video; is there a pending packet to decode? */
        while(ogg_stream_packetout(&to,&op)<1){
          while(ogg_sync_pageout(&oy,&og)<1){
            /* no data.  Grab another page */
            if ( feof(infile) || ! buffer_data(infile,&oy) ) {
              goto end_of_loop;
            }
          }
          queue_page(&og);
        }
        if(pp_inc){
          pp_level+=pp_inc;
          th_decode_ctl(td,TH_DECCTL_SET_PPLEVEL,&pp_level,
           sizeof(pp_level));
          pp_inc=0;
        }
        /*HACK: This should be set after a seek or a gap, but we might not have
           a granulepos for the first packet (we only have them for the last
           packet on a page), so we just set it as often as we get it.
          To do this right, we should back-track from the last packet on the
           page and compute the correct granulepos for the first packet after
           a seek or a gap.*/
        if(op.granulepos>=0){
          th_decode_ctl(td,TH_DECCTL_SET_GRANPOS,&op.granulepos,
           sizeof(op.granulepos));
        }
        if(th_decode_packetin(td,&op,&videobuf_granulepos)==0){
          video_write();
          frames++;
  
          /* is it already too old to be useful?  This is only actually
             useful cosmetically after a SIGSTOP.  Note that we have to
             decode the frame even if we don't show it (for now) due to
             keyframing.  Soon enough libtheora will be able to deal
             with non-keyframe seeks.  */
          /*If we are too slow, reduce the pp level.*/
          videobuf_time=th_granule_time(td,videobuf_granulepos);
          if ( videobuf_time < audiobuf_time ) {
            pp_inc=pp_level>0?-1:0;
            dropped++;
          }
        }
      } else {
        timeout.tv_sec=0;
        timeout.tv_usec= (videobuf_time - audiobuf_time)*1000000;
        select(0,&empty,&writefs,&empty,&timeout);
      }
    } else {
      // keep loop from burning too hot
      timeout.tv_sec=1;
      timeout.tv_usec=0;
      select(0,&empty,&writefs,&empty,&timeout);
    }
    // check for SDL events to exit
    if (SDL_PollEvent(&event) == 1) {
      if (event.type == SDL_KEYDOWN &&
          event.key.keysym.sym == SDLK_ESCAPE)
        break;
    }
  }
end_of_loop:
  SDL_PauseAudio(1);

  /* tear it all down */

  SDL_CloseAudio();
  SDL_Quit();

  if(vorbis_p){
    ogg_stream_clear(&vo);
    vorbis_block_clear(&vb);
    vorbis_dsp_clear(&vd);
    vorbis_comment_clear(&vc);
    vorbis_info_clear(&vi);
  }
  if(theora_p){
    ogg_stream_clear(&to);
    th_decode_free(td);
    th_comment_clear(&tc);
    th_info_clear(&ti);
  }
  ogg_sync_clear(&oy);

  if(infile && infile!=stdin)fclose(infile);

  fprintf(stderr,
          "\r                                                             \r");
  fprintf(stderr, "%d frames", frames);
  if (dropped) fprintf(stderr, " (%d dropped)", dropped);
  fprintf(stderr, "\n");
  fprintf(stderr, "\nDone.\n");

  return(0);

}

