/*
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "opus.h"

int
get_opus_metadata(PerlIO *infile, char *file, HV *info, HV *tags)
{
  return _opus_parse(infile, file, info, tags, 0);
}

#define OGG_HEADER_SIZE 28
int
_opus_parse(PerlIO *infile, char *file, HV *info, HV *tags, uint8_t seeking)
{
  Buffer ogg_buf, vorbis_buf;
  unsigned char *bptr;
  unsigned char *last_bptr;
  unsigned int buf_size;

  unsigned int id3_size = 0; // size of leading ID3 data

  off_t file_size;           // total file size
  off_t audio_size;          // total size of audio without tags
  off_t audio_offset = 0;    // offset to audio
  off_t seek_position;
  
  unsigned char ogghdr[OGG_HEADER_SIZE];
  char header_type;
  int serialno;
  int final_serialno;
  int pagenum;
  uint8_t num_segments;
  int pagelen;
  int page = 0;
  int packets = 0;
  int streams = 0;
  
  unsigned char opushdr[11];
  unsigned char channels;
  unsigned int samplerate = 0;
  unsigned int preskip = 0;
  unsigned int input_samplerate = 0;
  uint64_t granule_pos = 0;
  
  unsigned char TOC_byte = 0;

  int i;
  int err = 0;
  
  buffer_init(&ogg_buf, OGG_BLOCK_SIZE);
  buffer_init(&vorbis_buf, 0);
  
  file_size = _file_size(infile);
  my_hv_store( info, "file_size", newSVuv(file_size) );
  
  if ( !_check_buf(infile, &ogg_buf, 10, OGG_BLOCK_SIZE) ) {
    err = -1;
    goto out;
  }

  // Skip ID3 tags if any
  bptr = (unsigned char *)buffer_ptr(&ogg_buf);
  if (
    (bptr[0] == 'I' && bptr[1] == 'D' && bptr[2] == '3') &&
    bptr[3] < 0xff && bptr[4] < 0xff &&
    bptr[6] < 0x80 && bptr[7] < 0x80 && bptr[8] < 0x80 && bptr[9] < 0x80
  ) {
    /* found an ID3 header... */
    id3_size = 10 + (bptr[6]<<21) + (bptr[7]<<14) + (bptr[8]<<7) + bptr[9];

    if (bptr[5] & 0x10) {
      // footer present
      id3_size += 10;
    }
    
    buffer_clear(&ogg_buf);
    
    audio_offset += id3_size;
    
    DEBUG_TRACE("Skipping ID3v2 tag of size %d\n", id3_size);

    PerlIO_seek(infile, id3_size, SEEK_SET);
  }
  
  while (1) {
    // Grab 28-byte Ogg header
    if ( !_check_buf(infile, &ogg_buf, OGG_HEADER_SIZE, OGG_BLOCK_SIZE) ) {
      err = -1;
      goto out;
    }
    
    buffer_get(&ogg_buf, ogghdr, OGG_HEADER_SIZE);
    
    audio_offset += OGG_HEADER_SIZE;
    
    // check that the first four bytes are 'OggS'
    if ( ogghdr[0] != 'O' || ogghdr[1] != 'g' || ogghdr[2] != 'g' || ogghdr[3] != 'S' ) {
      PerlIO_printf(PerlIO_stderr(), "Not an Ogg file (bad OggS header): %s\n", file);
      goto out;
    }
  
    // Header type flag
    header_type = ogghdr[5];
    
    // Absolute granule position, used to find the first audio page
    bptr = ogghdr + 6;
    granule_pos = (uint64_t)CONVERT_INT32LE(bptr);
    bptr += 4;
    granule_pos |= (uint64_t)CONVERT_INT32LE(bptr) << 32;
    
    // Stream serial number
    serialno = CONVERT_INT32LE((ogghdr+14));
    
    // Count start-of-stream pages
    if ( header_type & 0x02 ) {
      streams++;
    }
    
    // Keep track of packet count
    if ( !(header_type & 0x01) ) {
      packets++;
    }
    
    // stop processing if we reach the 3rd packet and have no data
    if (packets > 2 * streams && !buffer_len(&vorbis_buf) ) {
      break;
    }
    
    // Page seq number
    pagenum = CONVERT_INT32LE((ogghdr+18));
    
    if (page >= 0 && page == pagenum) {
      page++;
    }
    else {
      page = -1;
      DEBUG_TRACE("Missing page(s) in Ogg file: %s\n", file);
    }

    DEBUG_TRACE("OggS page %d / packet %d at %d\n", pagenum, packets, (int)(audio_offset - OGG_HEADER_SIZE));
    DEBUG_TRACE("  granule_pos: %llu\n", granule_pos);
    
    // Number of page segments
    num_segments = ogghdr[26];
    
    // Calculate total page size
    pagelen = ogghdr[27];
    if (num_segments > 1) {
      int i;
      
      if ( !_check_buf(infile, &ogg_buf, num_segments, OGG_BLOCK_SIZE) ) {
        err = -1;
        goto out;
      }
      
      for( i = 0; i < num_segments - 1; i++ ) {
        u_char x;
        x = buffer_get_char(&ogg_buf);
        pagelen += x;
      }

      audio_offset += num_segments - 1;
    }
    
    if ( !_check_buf(infile, &ogg_buf, pagelen, OGG_BLOCK_SIZE) ) {
      err = -1;
      goto out;
    }
  
    // Still don't have enough data, must have reached the end of the file
    if ( buffer_len(&ogg_buf) < pagelen ) {
      PerlIO_printf(PerlIO_stderr(), "Premature end of file: %s\n", file);
    
      err = -1;
      goto out;
    }
    
    audio_offset += pagelen;

    // Copy page into vorbis buffer
    buffer_append( &vorbis_buf, buffer_ptr(&ogg_buf), pagelen );
    DEBUG_TRACE("  Read %d into vorbis buffer\n", pagelen);

    if ( granule_pos != 0 ) {
      // RFC7845: The granule position MUST be zero for the ID header
      //          page and the page where the comment header
      //          completes.
      //
      // So, we need to read more pages
      buffer_consume( &ogg_buf, pagelen );
      continue;
    }

    // Process vorbis packet
    TOC_byte = buffer_get_char(&vorbis_buf);
    if ( TOC_byte == 'O' ) {
      if ( strncmp( buffer_ptr(&vorbis_buf), "pusTags", 7 ) == 0) {
        buffer_consume(&vorbis_buf, 7);
        DEBUG_TRACE("  Found Opus tags TOC packet type\n");
      	if ( !seeking ) {
                _parse_vorbis_comments(infile, &vorbis_buf, tags, 0);
      	}
        DEBUG_TRACE("  parsed vorbis comments\n");

        buffer_clear(&vorbis_buf);
      }
      else {
      	// Verify 'OpusHead' string
      	if ( strncmp( buffer_ptr(&vorbis_buf), "pusHead", 7 ) ) {
      	  PerlIO_printf(PerlIO_stderr(), "Not an Opus file (bad opus header): %s\n", file);
      	  goto out;
      	}
      	buffer_consume( &vorbis_buf, 7 );

      	DEBUG_TRACE("  Found Opus header TOC packet type\n");
      	// Parse info
      	// Grab 23-byte Vorbis header
      	if ( buffer_len(&vorbis_buf) < 11 ) {
      	  PerlIO_printf(PerlIO_stderr(), "Not an Opus file (opus header too short): %s\n", file);
      	  goto out;
      	}

      	buffer_get(&vorbis_buf, opushdr, 11);

      	my_hv_store( info, "version", newSViv( opushdr[0] ) );

      	channels = opushdr[1];
      	my_hv_store( info, "channels", newSViv(channels) );
      	my_hv_store( info, "stereo", newSViv( channels == 2 ? 1 : 0 ) );

      	preskip = CONVERT_INT16LE((opushdr+2));
      	my_hv_store( info, "preskip", newSViv(preskip) );

      	samplerate = 48000; // Opus only supports 48k
      	my_hv_store( info, "samplerate", newSViv(samplerate) );

      	input_samplerate = CONVERT_INT32LE((opushdr+4));
      	my_hv_store( info, "input_samplerate", newSViv(input_samplerate) );

      	DEBUG_TRACE("  parsed opus info header\n");
      }
      buffer_clear(&vorbis_buf);
    }
    
    // Skip rest of this page
    buffer_consume( &ogg_buf, pagelen );
  }
  
  // audio_offset is 28 less because we read the Ogg header
  audio_offset -= OGG_HEADER_SIZE;

  // from the first packet past the comments
  my_hv_store( info, "audio_offset", newSViv(audio_offset) );
  
  audio_size = file_size - audio_offset;
  my_hv_store( info, "audio_size", newSVuv(audio_size) );
  
  my_hv_store( info, "serial_number", newSVuv(serialno) );

  // find the last Ogg page

#define BUF_SIZE 8500 // from vlc
  
  seek_position = file_size - BUF_SIZE;
  while (1) {
    if ( seek_position < audio_offset ) {
      seek_position = audio_offset;
    }

    // calculate average bitrate and duration
    DEBUG_TRACE("Seeking to %d to calculate bitrate/duration\n", (int)seek_position);
    PerlIO_seek(infile, seek_position, SEEK_SET);

    buffer_clear(&ogg_buf);

    if ( !_check_buf(infile, &ogg_buf, OGG_HEADER_SIZE, BUF_SIZE) ) {
      err = -1;
      goto out;
    }

    // Find sync
    bptr = (unsigned char *)buffer_ptr(&ogg_buf);
    buf_size = buffer_len(&ogg_buf);
    last_bptr = bptr;
    // make sure we have room for at least the one ogg page header
    while (buf_size >= OGG_HEADER_SIZE) {
      if (bptr[0] == 'O' && bptr[1] == 'g' && bptr[2] == 'g' && bptr[3] == 'S') {
        bptr += 6;

        // Get absolute granule value
        granule_pos = (uint64_t)CONVERT_INT32LE(bptr);
        bptr += 4;
        granule_pos |= (uint64_t)CONVERT_INT32LE(bptr) << 32;
        bptr += 4;
        DEBUG_TRACE("found granule_pos %llu / samplerate %d to calculate bitrate/duration\n", granule_pos, samplerate);
        //XXX: jump the header size
        last_bptr = bptr;
      }
      else {
        bptr++;
        buf_size--;
      }
    }
    bptr = last_bptr;

    // Get serial number of this page, if the serial doesn't match the beginning of the file
    // we have changed logical bitstreams and can't use the granule_pos for bitrate
    final_serialno = CONVERT_INT32LE((bptr));

    if ( granule_pos && samplerate && serialno == final_serialno ) {
      // XXX: needs to adjust for initial granule value if file does not start at 0 samples
      int length = (int)(((granule_pos-preskip) * 1.0 / samplerate) * 1000);
      my_hv_store( info, "song_length_ms", newSVuv(length) );
      my_hv_store( info, "bitrate_average", newSVuv( _bitrate(audio_size, length) ) );

      DEBUG_TRACE("Using granule_pos %llu / samplerate %d to calculate bitrate/duration\n", granule_pos, samplerate);
      break;
    }
    if ( seek_position == audio_offset ) {
      DEBUG_TRACE("Packet not found we won't be able to determine the length\n");
      break;
    }
    // seek backwards by BUF_SIZE - OGG_HEADER_SIZE so that if our previous sync happened to include the end
    // of page header we will include it in the next read
    seek_position -= (BUF_SIZE - OGG_HEADER_SIZE);
  }

out:
  buffer_free(&ogg_buf);
  buffer_free(&vorbis_buf);

  DEBUG_TRACE("Err %d\n", err);
  if (err) return err;

  return 0;
}

static off_t
opus_find_frame(PerlIO *infile, char *file, int offset)
{
  int frame_offset = -1;
  uint16_t preskip;
  uint32_t samplerate;
  uint32_t song_length_ms;
  uint64_t target_sample;
  HV *info = newHV();
  HV *tags = newHV();

  if (offset < 0) {
    return -1;
  }

  // We need to read all metadata first to get some data we need to calculate
  if ( _opus_parse(infile, file, info, tags, 1) != 0 ) {
    goto out;
  }

  song_length_ms = SvUV( *(my_hv_fetch( info, "song_length_ms" )) );
  if (offset >= song_length_ms) {
    goto out;
  }

  // Determine target sample we're looking for
  samplerate = SvIV( *(my_hv_fetch( info, "samplerate" )) );
  preskip = SvIV( *(my_hv_fetch( info, "preskip" )) );
  target_sample = (uint64_t)offset * samplerate / 1000;
  target_sample += preskip;

  DEBUG_TRACE("Looking for target sample %llu\n", target_sample);
  frame_offset = _ogg_binary_search_sample(infile, file, info, target_sample);

out:  
  // Don't leak
  SvREFCNT_dec(info);
  SvREFCNT_dec(tags);

  return frame_offset;
}
