/* Copyright (C) 2002 Jean-Marc Valin & Bernard Blackham

	File: speexcat.c

	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions
	are met:
	
	- Redistributions of source code must retain the above copyright
	notice, this list of conditions and the following disclaimer.
	
	- Redistributions in binary form must reproduce the above copyright
	notice, this list of conditions and the following disclaimer in the
	documentation and/or other materials provided with the distribution.
	
	- Neither the name of the Xiph.org Foundation nor the names of its
	contributors may be used to endorse or promote products derived from
	this software without specific prior written permission.
	
	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
	``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
	LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
	A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR
	CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
	EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
	PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
	LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
	NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
	SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <stdio.h>
#if !defined WIN32 && !defined _WIN32
#include <unistd.h>
#include <getopt.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <limits.h>

#include "speex.h"
#include "ogg/ogg.h"

#if defined WIN32 || defined _WIN32
#include <windows.h>
#include "getopt_win.h"
#include "wave_out.h"
/* We need the following two to set stdout to binary */
#include <io.h>
#include <fcntl.h>
#endif

#ifdef HAVE_SYS_SOUNDCARD_H
#include <sys/soundcard.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#endif

#include <string.h>
#include "wav_io.h"
#include "speex_header.h"
#include "speex_stereo.h"
#include "speex_callbacks.h"
#include "misc.h"

#define MAX_FRAME_SIZE 2000

#define readint(buf, base) (((buf[base+3]<<24)&0xff000000)| \
									((buf[base+2]<<16)&0xff0000)| \
									((buf[base+1]<<8)&0xff00)| \
  				  		 (buf[base]&0xff))

static void print_comments(char *comments, int length)
{
	char *c=comments;
	int len, i, nb_fields;

	len=readint(c, 0);
	c+=4;
	fwrite(c, 1, len, stderr);
	c+=len;
	fprintf (stderr, "; ");
	nb_fields=readint(c, 0);
	c+=4;
	for (i=0;i<nb_fields;i++)
	{
		len=readint(c, 0);
		c+=4;
		fwrite(c, 1, len, stderr);
		c+=len;
		fprintf (stderr, "; ");
	}
	fprintf(stderr, "\n");
}

void comment_init(char **comments, int* length, char *vendor_string);
void comment_add(char **comments, int* length, char *tag, char *val);

void usage()
{
	fprintf (stderr, "Usage: speexcat [options] <filespec1> [<filespec2> [<filespec3> [...]]]\n");
	fprintf (stderr, "Concatenates several speex files into one\n");
	fprintf (stderr, "\n");
	fprintf (stderr, "filespecN is a group of 3 parameters - filename start end\n");
	fprintf (stderr, "  filename             regular Speex file\n");
	fprintf (stderr, "  start                time to start within file\n");
	fprintf (stderr, "  end                  time to start within file\n");
	fprintf (stderr, " times may be given as ss, ss.ss, mm:ss, hh:mm:ss\n");
	fprintf (stderr, "  eg, 1:30.52, 1.43, 3:20, 3:20.18, etc\n");
	fprintf (stderr, "\n");
	fprintf (stderr, "Options:\n");
	fprintf (stderr, " --output-file file    Output file (default is STDOUT)\n");
	fprintf (stderr, " -h, --help            This help\n");
	fprintf (stderr, " -v, --version         Version information\n");
	fprintf (stderr, " --comment             Add the given string as an extra comment. This may be\n");
	fprintf (stderr, "                          used multiple times.\n");
	fprintf (stderr, " --author              Author of this track.\n");
	fprintf (stderr, " --title               Title for this track.\n");
	fprintf (stderr, "\n");
	fprintf (stderr, "Each input file must have the same rate/channels/encoding else the results\n");
	fprintf (stderr, "will be unpredictable ;-)\n");
	fprintf (stderr, "\n");
	fprintf (stderr, "Please report bugs to bernard@blackham.com.au\n");
}

void version()
{
	fprintf (stderr, "speexcat (Speex concatenator) version " VERSION " (compiled " __DATE__ ")\n");
	fprintf (stderr, "Copyright (C) 2003 Bernard Blackham\n");
}

/*Write an Ogg page to a file pointer*/
int oe_write_page(ogg_page *page, FILE *fp)
{
	int written;
	written = fwrite(page->header,1,page->header_len, fp);
	written += fwrite(page->body,1,page->body_len, fp);
	
	return written;
}

static void *process_header(ogg_packet *op, int enh_enabled, int *frame_size, int *rate, int *nframes, int forceMode, int *channels, SpeexStereoState *stereo)
{
	void *st;
	SpeexMode *mode;
	SpeexHeader *header;
	int modeID;
	SpeexCallback callback;
		
	header = speex_packet_to_header((char*)op->packet, op->bytes);
	if (!header)
	{
		fprintf (stderr, "Cannot read header\n");
		return NULL;
	}
	if (header->mode >= SPEEX_NB_MODES)
	{
		fprintf (stderr, "Mode number %d does not (any longer) exist in this version\n", 
					header->mode);
		return NULL;
	}
		
	modeID = header->mode;
	if (forceMode!=-1)
		modeID = forceMode;
	mode = speex_mode_list[modeID];
	
	if (mode->bitstream_version < header->mode_bitstream_version)
	{
		fprintf (stderr, "The file was encoded with a newer version of Speex. You need to upgrade in order to play it.\n");
		return NULL;
	}
	if (mode->bitstream_version > header->mode_bitstream_version) 
	{
		fprintf (stderr, "The file was encoded with an older version of Speex. You would need to downgrade the version in order to play it.\n");
		return NULL;
	}
	
	callback.callback_id = SPEEX_INBAND_STEREO;
	callback.func = speex_std_stereo_request_handler;
	callback.data = stereo;
	
	if (!*rate)
		*rate = header->rate;
	/* Adjust rate if --force-* options are used */
	if (forceMode!=-1)
	{
		if (header->mode < forceMode)
			*rate <<= (forceMode - header->mode);
		if (header->mode > forceMode)
			*rate >>= (header->mode - forceMode);
	}


	*nframes = header->frames_per_packet;

	if (*channels==-1)
		*channels = header->nb_channels;
	
	fprintf (stderr, "Reading %d Hz audio using %s mode", 
				*rate, mode->modeName);

	if (header->vbr)
		fprintf (stderr, " (VBR)\n");
	else
		fprintf(stderr, "\n");
	/*fprintf (stderr, "Decoding %d Hz audio at %d bps using %s mode\n", 
	 *rate, mode->bitrate, mode->modeName);*/

	free(header);
	return st;
}

void throw_parse_error(char* timestr, int pos) {
	int i;
	fprintf(stderr, "speexcat: Error while parsing time ");
	for (i=0; i <= pos; i++) {
		fprintf(stderr, "%c", timestr[i]);
	}
	fprintf(stderr," -=>%s\n", timestr+pos);
	exit(1);
}

int time_to_packet(char* timestr) {
	int packets_per_second = 50; // FIXME!!!
	// syntax as described by a regexp:
	// /(($hours:)?($minutes:))?$seconds(.$hundredths)?/
	// or more precisely in perl re syntax:
	// /(?:(?:(\d+):)?(\d+):)?(\d+)(?:\.\d\d?)?/
	// ie, allowable expressions:
	//    10 == 10 seconds
	//    10.1 == 10.1 seconds
	//    10:1 == 10 minutes, 1 second
	//    10:01 == 10 minutes, 1 second
	//    10:01.1 == 10 minutes, 1.1 seconds
	//    3:40:51 == 3 hours, 40 minutes, 51 seconds
	//    2:10:33.13 == 2 hours, 10 minutes, 33.13 seconds
	// damn this would be easy in perl :)
	
	// find sequences of numbers until you hit a : or . and figure the rest out
	// accordingly
	int timebits[4] = {0,0,0,0}; // hours, minutes, seconds, splitseconds
	int cursegment = 2;
	int numsegments = 0;
	char* tmp;
	int tmp2;
	char* curptr = timestr;

	// deal with split seconds (max 2 digits)
	if (curptr = strstr(timestr, ".")) {
		curptr++;
		for (tmp = curptr; *tmp; tmp++) {
			if ((*tmp < '0') || (*tmp > '9')) throw_parse_error(timestr, curptr-timestr);
		}
		timebits[3] = atoi(curptr);
		if (timebits[3] > 99) throw_parse_error(timestr, curptr-timestr);
		if (*(curptr+1)==0) timebits[3] *= 10; // only one digit
		curptr--;
		*curptr = 0;
	}
	
	// now the rest of the time spec
	curptr = timestr;
	while (*curptr) {
		if (*curptr >= '0' && *curptr <= '9') {
			timebits[cursegment] *= 10;
			timebits[cursegment] += *curptr - '0';
		} else if (*curptr = ':') {
			numsegments++;
			if (numsegments > 2) throw_parse_error(timestr, curptr-timestr);
			for (tmp2 = 0; tmp2 < 2; tmp2++) {
				timebits[tmp2] = timebits[tmp2+1];
			}
			timebits[2] = 0;
		} else throw_parse_error(timestr, curptr-timestr);
		curptr++;
	}
	
	//fprintf(stderr, "timebits: %d:%d:%d.%d\n", timebits[0], timebits[1], timebits[2], timebits[3]);
	return (int)((((timebits[0]*60)+timebits[1])*60+timebits[2])*packets_per_second+(timebits[3]*packets_per_second/100));
}

int main(int argc, char **argv)
{
	int c;
	int option_index = 0;
	char *inFile, *outFile = (char*)malloc(1024);
	FILE *fin, *fout=NULL;
	short out[MAX_FRAME_SIZE];
	float output[MAX_FRAME_SIZE];
	int frame_size=0;
	void *st=NULL;
	int read_packet_count, start_packet, end_packet;
	int write_packet_count=0;
	int file_packet_count;
	int read_stream_init = 0;
	int cur_granulepos = 0, old_granulepos;
	int new_comments = 0;
	char *vendor_string = "Encoded with Speex " VERSION;
	char *comments;
	int comments_length;
	struct option long_options[] =
	{
		{"help", no_argument, NULL, 0},
		{"version", no_argument, NULL, 0},
		{"output-file", required_argument, NULL, 0},
		{"comment", required_argument, NULL, 0},
		{"author", required_argument, NULL, 0},
		{"title", required_argument, NULL, 0},
		{0, 0, 0, 0}
	};
	ogg_sync_state oy;
	ogg_page		 og, oog;
	ogg_packet	  op;
	ogg_stream_state os, oos;
	int enh_enabled;
	int nframes=2;
	int close_in=0,close_out=0;
	int fileNum;
	int ret,bytes_written;
	int eos=0;
	int forceMode=-1;
	int audio_size=0;
	float loss_percent=-1;
	SpeexStereoState stereo = SPEEX_STEREO_STATE_INIT;
	int channels=-1;
	int rate=0;

	comment_init(&comments, &comments_length, vendor_string);

	enh_enabled = 0;

	/*Process options*/
	strcpy(outFile, "-");
	while(1)
	{
		c = getopt_long (argc, argv, "hvo",
							  long_options, &option_index);
		if (c==-1)
			break;
		
		switch(c)
		{
		case 0:
			if (strcmp(long_options[option_index].name,"help")==0)
			{
				usage();
				exit(0);
			} else if (strcmp(long_options[option_index].name,"version")==0)
			{
				version();
				exit(0);
			} else if (strcmp(long_options[option_index].name,"output-file")==0)
			{
				strcpy(outFile, optarg);
			} else if (strcmp(long_options[option_index].name,"comment")==0)
			{
			  comment_add(&comments, &comments_length, NULL, optarg); 
			  new_comments = 1;
			} else if (strcmp(long_options[option_index].name,"author")==0)
			{
			  comment_add(&comments, &comments_length, "author=", optarg); 
			  new_comments = 1;
			} else if (strcmp(long_options[option_index].name,"title")==0)
			{
			  comment_add(&comments, &comments_length, "title=", optarg); 
			  new_comments = 1;
			}
			break;
		case 'h':
			usage();
			exit(0);
			break;
		case 'v':
			version();
			exit(0);
			break;
		case '?':
			usage();
			exit(1);
			break;
	  case 'o':
		 strcpy(outFile, optarg);
		 break;
		}
	}
	
	if ((argc-optind == 0) || ((argc-optind)%3 != 0))
	{
		usage();
		exit(1);
	}

	// Initialise output file
	/*Initialize Ogg stream struct*/
	srand(time(NULL));
	if (ogg_stream_init(&oos, rand())==-1)
	{
		fprintf(stderr,"Output stream init failed\n");
		exit(1);
	}
	if (strcmp(outFile,"-")==0)
	{
#if defined WIN32 || defined _WIN32
		_setmode(_fileno(stdout), _O_BINARY);
#endif
		fout=stdout;
	}
	else 
	{
#if defined WIN32 || defined _WIN32
		fout = fopen(outFile, "wb");
#else
		fout = fopen(outFile, "w");
#endif
		if (!fout)
		{
			perror(outFile);
			exit(1);
		}
		close_out=1;
	}

	for (fileNum=0; fileNum < argc-optind; fileNum+=3) {
		inFile       = argv[fileNum+optind];
		//start_packet = atoi(argv[fileNum+optind+1]);
		start_packet = time_to_packet(argv[fileNum+optind+1]);
		end_packet   = time_to_packet(argv[fileNum+optind+2]);
		//end_packet   = atoi(argv[fileNum+optind+2]);

		fprintf(stderr, "*** Reading file %s %d-%d ... ", inFile, start_packet, end_packet);
		start_packet+=2; // add 2 for header packets
		if (end_packet != 0) {
			end_packet+=2;
		}
		file_packet_count = 0;

		read_packet_count = 0;
		read_stream_init = 0;
		old_granulepos = -1;
		eos = 0;
		frame_size=0;
		st=NULL;
		//nframes=2;
		//forceMode=-1;
		//audio_size=0;
		//loss_percent=-1;
		//channels=-1;
		//rate=0;

		/*Open input file*/
		if (strcmp(inFile, "-")==0)
		{
#if defined WIN32 || defined _WIN32
			_setmode(_fileno(stdin), _O_BINARY);
#endif
			fin=stdin;
		}
		else 
		{
#if defined WIN32 || defined _WIN32
			fin = fopen(inFile, "rb");
#else
			fin = fopen(inFile, "r");
#endif
			if (!fin)
			{
				perror(inFile);
				exit(1);
			}
			close_in=1;
		}


		/*Init Ogg data struct*/
		ogg_sync_init(&oy);
		
		/*Main decoding loop*/
		while (1)
		{
			char *data;
			int i, j, nb_read;
			/*Get the ogg buffer for writing*/
			data = ogg_sync_buffer(&oy, 200);
			/*Read bitstream from input file*/
			nb_read = fread(data, sizeof(char), 200, fin);		
			ogg_sync_wrote(&oy, nb_read);

			/*Loop for all complete pages we got (most likely only one)*/
			while (ogg_sync_pageout(&oy, &og)==1)
			{
				if (read_stream_init == 0) {
					ogg_stream_init(&os, ogg_page_serialno(&og));
					read_stream_init = 1;
				}
				/*Add page to the bitstream*/
				ogg_stream_pagein(&os, &og);
				/*Extract all available packets*/
				while (!eos && ogg_stream_packetout(&os, &op)==1)
				{
					if (op.b_o_s && (fileNum != 0)) {
						op.b_o_s = 0;
					}
					/*If first packet, process as Speex header*/
					if (read_packet_count==0)
					{
						st = process_header(&op, enh_enabled, &frame_size, &rate, &nframes, forceMode, &channels, &stereo);
						if (!nframes)
							nframes=1;
						if (!st)
							exit(1);
						if (fileNum == 0) {
							op.granulepos = 0;
							ogg_stream_packetin(&oos, &op);
							write_packet_count++;
							file_packet_count++;
						}
					} else if (read_packet_count==1){
						print_comments((char*)op.packet, op.bytes);
						// munge comments
						if (fileNum == 0) {
							if (new_comments) {
								op.packet = (unsigned char *)comments;
								op.bytes = comments_length;
							}
							op.b_o_s = 0;
							op.e_o_s = 0;
							op.granulepos = 0;
							op.packetno = 1;
							ogg_stream_packetin(&oos, &op);
							write_packet_count++;
							file_packet_count++;
						}
					} else {
						/*End of stream condition*/
						if (op.e_o_s) {
							eos=1;
							if (fileNum != argc-optind-3) {
								op.e_o_s = 0;
							}
						}
						op.packetno = write_packet_count;
						if ((read_packet_count >= start_packet) && ((read_packet_count <= end_packet) || (end_packet == 0))) {
							// set EOS if needed
							if ((read_packet_count == end_packet) && (fileNum == argc-optind-3)) {
								// fprintf(stderr, "Setting new EOS on file %d at packet %d where nframes=%d\n", fileNum, op.granulepos, nframes);
								op.e_o_s = 1;
								eos=1;
							}
							// granulepos is the number of PCM frames from the
							// beginning which is the speex frame number * nframes
							// where nframes is the number of PCM frames per speex
							// packet (from process_header) but because nframes
							// never seems to be set properly, we'll just dredge
							// the actual granulepos's from the original files
							// old code: op.granulepos = (write_packet_count-2)*nframes;
							if (op.granulepos != -1 && old_granulepos != -1 && old_granulepos <= op.granulepos) {
								cur_granulepos += (op.granulepos - old_granulepos);
								// fprintf(stderr, "incrementing by (%d - %d) : new granule pos is %d\n", old_granulepos, op.granulepos, cur_granulepos);
							}
							if (op.granulepos != -1) old_granulepos=op.granulepos;
							op.granulepos = cur_granulepos;
							ogg_stream_packetin(&oos, &op);
							write_packet_count++;
							file_packet_count++;
						} else {
							if (op.granulepos != -1) {
								old_granulepos=op.granulepos;
							}
						}
					}
					read_packet_count++;
				}
			}
			/*Write all new pages (most likely 0 or 1)*/
			while (ogg_stream_pageout(&oos,&oog))
			{
				ret = oe_write_page(&oog, fout);
				if(ret != oog.header_len + oog.body_len)
				{
					fprintf (stderr,"Failed writing header to output stream\n");
					exit(1);
				}
				else
					bytes_written += ret;
			}
			if (feof(fin))
				break;

		}

		ogg_sync_clear(&oy);
		ogg_stream_clear(&os);
#if defined WIN32 || defined _WIN32
		if (strlen(outFile)==0)
			WIN_Audio_close ();
#endif

		if (close_in)
			fclose(fin);
	
		// fprintf(stderr, "Wrote %d packets :)\n", file_packet_count);
	}
	while (ogg_stream_flush(&oos, &oog))
	{
		ret = oe_write_page(&oog, fout);
		if(ret != oog.header_len + oog.body_len)
		{
			fprintf (stderr,"Failed writing header to output stream\n");
			exit(1);
		}
		else
			bytes_written += ret;
	}
	ogg_stream_clear(&oos);
	
	if (fout != NULL)
		fclose(fout);	


	return 1;
}

/*                 
 Comments will be stored in the Vorbis style.            
 It is describled in the "Structure" section of
    http://www.xiph.org/ogg/vorbis/doc/v-comment.html

The comment header is decoded as follows:
  1) [vendor_length] = read an unsigned integer of 32 bits
  2) [vendor_string] = read a UTF-8 vector as [vendor_length] octets
  3) [user_comment_list_length] = read an unsigned integer of 32 bits
  4) iterate [user_comment_list_length] times {
     5) [length] = read an unsigned integer of 32 bits
     6) this iteration's user comment = read a UTF-8 vector as [length] octets
     }
  7) [framing_bit] = read a single bit as boolean
  8) if ( [framing_bit]  unset or end of packet ) then ERROR
  9) done.

  If you have troubles, please write to ymnk@jcraft.com.
 */

#define readint(buf, base) (((buf[base+3]<<24)&0xff000000)| \
                           ((buf[base+2]<<16)&0xff0000)| \
                           ((buf[base+1]<<8)&0xff00)| \
  	           	    (buf[base]&0xff))
#define writeint(buf, base, val) do{ buf[base+3]=((val)>>24)&0xff; \
                                     buf[base+2]=((val)>>16)&0xff; \
                                     buf[base+1]=((val)>>8)&0xff; \
                                     buf[base]=(val)&0xff; \
                                 }while(0)

void comment_init(char **comments, int* length, char *vendor_string)
{
  int vendor_length=strlen(vendor_string);
  int user_comment_list_length=0;
  int len=4+vendor_length+4;
  char *p=(char*)malloc(len);
  if(p==NULL){
  }
  writeint(p, 0, vendor_length);
  memcpy(p+4, vendor_string, vendor_length);
  writeint(p, 4+vendor_length, user_comment_list_length);
  *length=len;
  *comments=p;
}
void comment_add(char **comments, int* length, char *tag, char *val)
{
  char* p=*comments;
  int vendor_length=readint(p, 0);
  int user_comment_list_length=readint(p, 4+vendor_length);
  int tag_len=(tag?strlen(tag):0);
  int val_len=strlen(val);
  int len=(*length)+4+tag_len+val_len;

  p=(char*)realloc(p, len);
  if(p==NULL){
  }

  writeint(p, *length, tag_len+val_len);      /* length of comment */
  if(tag) memcpy(p+*length+4, tag, tag_len);  /* comment */
  memcpy(p+*length+4+tag_len, val, val_len);  /* comment */
  writeint(p, 4+vendor_length, user_comment_list_length+1);

  *comments=p;
  *length=len;
}
#undef readint
#undef writeint

