/* This program is licensed under the GNU Library General Public License, version 2,
 * a copy of which is included with this program (LICENCE.LGPL).
 *
 * (c) 2000-2001 Michael Smith <msmith@labyrinth.net.au>
 *
 *
 * Comment editing backend, suitable for use by nice frontend interfaces.
 *
 * last modified: $Id: vcedit.c 1390 2005-06-14 17:46:40Z robert $
 */


#include <stdlib.h>
#include <string.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include <errno.h>
#include <ogg/ogg.h>
#include <vorbis/codec.h>


#include "fileio.h"
#include "vcedit.h"
#include "i18n.h"

#define CHUNKSIZE 4096

void skipID3v2Tag(TFILE *in)
{
     char tag[4], sig[5];
     int  found = 0;
     long savedLoc = ttell(in);

     if (tread(tag, sizeof(char), 3, in) != 3)
     {
         tseek(in, savedLoc, SEEK_SET);
         return;
     }

     if (strncmp(tag, "ID3", 3) != 0)
     {
         tseek(in, savedLoc, SEEK_SET);
         return;
     }

     /* Now look for the OggS header */
     memset(sig, 0, 5);
     for(;;)
     {
         if (tread(&sig[3], sizeof(char), 1, in) != 1)
         {
             tclose(in);
             return;
         }

         if (strncmp("OggS", sig, 4) == 0)
         {
             tseek(in, ttell(in) - 4, SEEK_SET);
             found = 1;
             break;
         }
         memmove(sig, sig + 1, 3);
     }
     if (!found)
     {
         tseek(in, savedLoc, SEEK_SET);
         return;
     }
}

vcedit_state *vcedit_new_state(void)
{
	vcedit_state *state = malloc(sizeof(vcedit_state));
	memset(state, 0, sizeof(vcedit_state));

	return state;
}

char *vcedit_error(vcedit_state *state)
{
	return state->lasterror;
}

vorbis_comment *vcedit_comments(vcedit_state *state)
{
	return state->vc;
}

static void vcedit_clear_internals(vcedit_state *state)
{
	if(state->vc)
	{
		vorbis_comment_clear(state->vc);
		free(state->vc);
	}
	if(state->os)
	{
		ogg_stream_clear(state->os);
		free(state->os);
	}
	if(state->oy)
	{
		ogg_sync_clear(state->oy);
		free(state->oy);
	}
	if(state->vendor)
		free(state->vendor);
    if(state->mainbuf)
        free(state->mainbuf);
    if(state->bookbuf)
        free(state->bookbuf);
    if(state->vi) {
       	vorbis_info_clear(state->vi);
        free(state->vi);
    }

    memset(state, 0, sizeof(*state));
}

void vcedit_clear(vcedit_state *state)
{
	if(state)
	{
		vcedit_clear_internals(state);
		free(state);
	}
}

/* Next two functions pulled straight from libvorbis, apart from one change
 * - we don't want to overwrite the vendor string.
 */
static void _v_writestring(oggpack_buffer *o,char *s, int len)
{
	while(len--)
	{
		oggpack_write(o,*s++,8);
	}
}

static int _commentheader_out(vorbis_comment *vc, char *vendor, ogg_packet *op)
{
	oggpack_buffer opb;

	oggpack_writeinit(&opb);

	/* preamble */  
	oggpack_write(&opb,0x03,8);
	_v_writestring(&opb,"vorbis", 6);

	/* vendor */
	oggpack_write(&opb,strlen(vendor),32);
	_v_writestring(&opb,vendor, strlen(vendor));

	/* comments */
	oggpack_write(&opb,vc->comments,32);
	if(vc->comments){
		int i;
		for(i=0;i<vc->comments;i++){
			if(vc->user_comments[i]){
				oggpack_write(&opb,vc->comment_lengths[i],32);
				_v_writestring(&opb,vc->user_comments[i], 
                        vc->comment_lengths[i]);
			}else{
				oggpack_write(&opb,0,32);
			}
		}
	}
	oggpack_write(&opb,1,1);

	op->packet = malloc(oggpack_bytes(&opb));
	memcpy(op->packet, opb.buffer, oggpack_bytes(&opb));

	op->bytes=oggpack_bytes(&opb);
	op->b_o_s=0;
	op->e_o_s=0;
	op->granulepos=0;

	oggpack_writeclear(&opb);
	return 0;
}

static int _blocksize(vcedit_state *s, ogg_packet *p)
{
	int this = vorbis_packet_blocksize(s->vi, p);
	int ret = (this + s->prevW)/4;

	if(!s->prevW)
	{
		s->prevW = this;
		return 0;
	}

	s->prevW = this;
	return ret;
}

static int _fetch_next_packet(vcedit_state *s, ogg_packet *p, ogg_page *page)
{
	int result;
	char *buffer;
	int bytes;

	result = ogg_stream_packetout(s->os, p);

	if(result > 0)
		return 1;
	else
	{
		if(s->eosin)
			return 0;
		while(ogg_sync_pageout(s->oy, page) <= 0)
		{
			buffer = ogg_sync_buffer(s->oy, CHUNKSIZE);
			bytes = s->read(buffer,1, CHUNKSIZE, s->in);
			ogg_sync_wrote(s->oy, bytes);
			if(bytes == 0) 
				return 0;
		}
		if(ogg_page_eos(page))
			s->eosin = 1;
		else if(ogg_page_serialno(page) != s->serial)
		{
			s->eosin = 1;
			s->extrapage = 1;
			return 0;
		}

		ogg_stream_pagein(s->os, page);
		return _fetch_next_packet(s, p, page);
	}
}

int vcedit_open(vcedit_state *state, TFILE *in)
{
        skipID3v2Tag(in);
	return vcedit_open_callbacks(state, (void *)in, 
			(vcedit_read_func)tread, (vcedit_write_func)twrite);
}

int vcedit_open_callbacks(vcedit_state *state, void *in,
		vcedit_read_func read_func, vcedit_write_func write_func)
{

	char *buffer;
	int bytes,i;
	ogg_packet *header;
	ogg_packet	header_main;
	ogg_packet  header_comments;
	ogg_packet	header_codebooks;
	ogg_page    og;

	state->in = in;
	state->read = read_func;
	state->write = write_func;

	state->oy = malloc(sizeof(ogg_sync_state));
	ogg_sync_init(state->oy);

	buffer = ogg_sync_buffer(state->oy, CHUNKSIZE);
	bytes = state->read(buffer, 1, CHUNKSIZE, state->in);

	ogg_sync_wrote(state->oy, bytes);

	if(ogg_sync_pageout(state->oy, &og) != 1)
	{
		if(bytes<CHUNKSIZE)
			state->lasterror = _("Input truncated or empty.");
		else
			state->lasterror = _("Input is not an Ogg bitstream.");
		goto err;
	}

	state->serial = ogg_page_serialno(&og);

	state->os = malloc(sizeof(ogg_stream_state));
	ogg_stream_init(state->os, state->serial);

        state->vi = malloc(sizeof(vorbis_info));
	vorbis_info_init(state->vi);

	state->vc = malloc(sizeof(vorbis_comment));
	vorbis_comment_init(state->vc);

	if(ogg_stream_pagein(state->os, &og) < 0)
	{
		state->lasterror = _("Error reading first page of Ogg bitstream.");
		goto err;
	}

	if(ogg_stream_packetout(state->os, &header_main) != 1)
	{
		state->lasterror = _("Error reading initial header packet.");
		goto err;
	}

	if(vorbis_synthesis_headerin(state->vi, state->vc, &header_main) < 0)
	{
		state->lasterror = _("Ogg bitstream does not contain vorbis data.");
		goto err;
	}

	state->mainlen = header_main.bytes;
	state->mainbuf = malloc(state->mainlen);
	memcpy(state->mainbuf, header_main.packet, header_main.bytes);

	i = 0;
	header = &header_comments;
	while(i<2) {
		while(i<2) {
			int result = ogg_sync_pageout(state->oy, &og);
			if(result == 0) break; /* Too little data so far */
			else if(result == 1)
			{
				ogg_stream_pagein(state->os, &og);
				while(i<2)
				{
					result = ogg_stream_packetout(state->os, header);
					if(result == 0) break;
					if(result == -1)
					{
						state->lasterror = _("Corrupt secondary header.");
						goto err;
					}
					vorbis_synthesis_headerin(state->vi, state->vc, header);
					if(i==1)
					{
						state->booklen = header->bytes;
						state->bookbuf = malloc(state->booklen);
						memcpy(state->bookbuf, header->packet, 
								header->bytes);
					}
					i++;
					header = &header_codebooks;
				}
			}
		}

		buffer = ogg_sync_buffer(state->oy, CHUNKSIZE);
		bytes = state->read(buffer, 1, CHUNKSIZE, state->in);
		if(bytes == 0 && i < 2)
		{
			state->lasterror = _("EOF before end of vorbis headers.");
			goto err;
		}
		ogg_sync_wrote(state->oy, bytes);
	}

	/* Copy the vendor tag */
	state->vendor = malloc(strlen(state->vc->vendor) +1);
	strcpy(state->vendor, state->vc->vendor);

	/* Headers are done! */
	return 0;

err:
	vcedit_clear_internals(state);
	return -1;
}

int vcedit_write(vcedit_state *state, void *out)
{
	ogg_stream_state streamout;
	ogg_packet header_main;
	ogg_packet header_comments;
	ogg_packet header_codebooks;

	ogg_page ogout, ogin;
	ogg_packet op;
	ogg_int64_t granpos = 0;
	int result;
	char *buffer;
	int bytes;
	int needflush=0, needout=0;

	state->eosin = 0;
	state->extrapage = 0;

	header_main.bytes = state->mainlen;
	header_main.packet = state->mainbuf;
	header_main.b_o_s = 1;
	header_main.e_o_s = 0;
	header_main.granulepos = 0;

	header_codebooks.bytes = state->booklen;
	header_codebooks.packet = state->bookbuf;
	header_codebooks.b_o_s = 0;
	header_codebooks.e_o_s = 0;
	header_codebooks.granulepos = 0;

	ogg_stream_init(&streamout, state->serial);

	_commentheader_out(state->vc, state->vendor, &header_comments);

	ogg_stream_packetin(&streamout, &header_main);
	ogg_stream_packetin(&streamout, &header_comments);
	ogg_stream_packetin(&streamout, &header_codebooks);

	while((result = ogg_stream_flush(&streamout, &ogout)) != 0)
	{
		if(state->write(ogout.header,1,ogout.header_len, out) !=
				(size_t) ogout.header_len)
			goto cleanup;
		if(state->write(ogout.body,1,ogout.body_len, out) != 
				(size_t) ogout.body_len)
			goto cleanup;
	}

	while(_fetch_next_packet(state, &op, &ogin))
	{
		int size;
		size = _blocksize(state, &op);
		granpos += size;

		if(needflush)
		{
			if(ogg_stream_flush(&streamout, &ogout))
			{
				if(state->write(ogout.header,1,ogout.header_len, 
							out) != (size_t) ogout.header_len)
					goto cleanup;
				if(state->write(ogout.body,1,ogout.body_len, 
							out) != (size_t) ogout.body_len)
					goto cleanup;
			}
		}
		else if(needout)
		{
			if(ogg_stream_pageout(&streamout, &ogout))
			{
				if(state->write(ogout.header,1,ogout.header_len, 
							out) != (size_t) ogout.header_len)
					goto cleanup;
				if(state->write(ogout.body,1,ogout.body_len, 
							out) != (size_t) ogout.body_len)
					goto cleanup;
			}
		}

		needflush=needout=0;

		if(op.granulepos == -1)
		{
			op.granulepos = granpos;
			ogg_stream_packetin(&streamout, &op);
		}
		else /* granulepos is set, validly. Use it, and force a flush to 
				account for shortened blocks (vcut) when appropriate */ 
		{
			if(granpos > op.granulepos)
			{
				granpos = op.granulepos;
				ogg_stream_packetin(&streamout, &op);
				needflush=1;
			}
			else 
			{
				ogg_stream_packetin(&streamout, &op);
				needout=1;
			}
		}		
	}

	streamout.e_o_s = 1;
	while(ogg_stream_flush(&streamout, &ogout))
	{
		if(state->write(ogout.header,1,ogout.header_len, 
					out) != (size_t) ogout.header_len)
			goto cleanup;
		if(state->write(ogout.body,1,ogout.body_len, 
					out) != (size_t) ogout.body_len)
			goto cleanup;
	}

	if (state->extrapage)
	{
		if(state->write(ogin.header,1,ogin.header_len,
		                out) != (size_t) ogin.header_len)
			goto cleanup;
		if (state->write(ogin.body,1,ogin.body_len, out) !=
				(size_t) ogin.body_len)
			goto cleanup;
	}

	state->eosin=0; /* clear it, because not all paths to here do */
	while(!state->eosin) /* We reached eos, not eof */
	{
		/* We copy the rest of the stream (other logical streams)
		 * through, a page at a time. */
		while(1)
		{
			result = ogg_sync_pageout(state->oy, &ogout);
			if(result==0)
                break;
			if(result<0)
				state->lasterror = _("Corrupt or missing data, continuing...");
			else
			{
				/* Don't bother going through the rest, we can just 
				 * write the page out now */
				if(state->write(ogout.header,1,ogout.header_len, 
						out) != (size_t) ogout.header_len) {
                    fprintf(stderr, "Bumming out\n");
					goto cleanup;
                }
				if(state->write(ogout.body,1,ogout.body_len, out) !=
						(size_t) ogout.body_len) {
                    fprintf(stderr, "Bumming out 2\n");
					goto cleanup;
                }
			}
		}
		buffer = ogg_sync_buffer(state->oy, CHUNKSIZE);
		bytes = state->read(buffer,1, CHUNKSIZE, state->in);
		ogg_sync_wrote(state->oy, bytes);
		if(bytes == 0) 
		{
			state->eosin = 1;
			break;
		}
	}						

cleanup:
	
        free(header_comments.packet);
	ogg_stream_clear(&streamout);

	free(state->mainbuf);
	free(state->bookbuf);
        state->mainbuf = state->bookbuf = NULL;

	if(!state->eosin)
	{
		state->lasterror =
			_("Error writing stream to output. "
			"Output stream may be corrupted or truncated.");
		return -1;
	}

	return 0;
}

