/*
 *  playlist-mp3.c
 *  mod_musicindex
 *
 *  $Id: playlist-mp3.c 1010 2012-08-07 13:38:34Z varenet $
 *
 *  Created by Regis BOUDIN on Thu Jan 22 2004.
 *  Copyright (c) 2003-2004 Regis BOUDIN
 *  Copyright (c) 2003-2009 Thibaut VARENE
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License version 2.1,
 *  as published by the Free Software Foundation.
 *
 */

/**
 * @file
 * MP3 files management system.
 *
 * @author Regis Boudin
 * @author Thibaut Varene
 * @version $Revision: 1010 $
 * @date 2003-2009
 *
 * This file contains everything needed to produce music entries from
 * MP3 files. @n
 * Some pieces of code are based on xmms mpg123 and mad plugins. @n
 * DEV NOTE: As one might guess, MP3 files are a real mess :P
 *
 * @todo Find out if we trash "detect by content".
 */

#include "playlist.h"
#include "playlist-mp3.h"

#include <mad.h>        /* libmad */
#include <id3tag.h>	/* id3 tags */
#include <ctype.h>      /* isdigit */
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#define INPUT_BUFFER_SIZE       16*1024	/**< Input buffer size used by libmad subsystem. */
#define MAD_AVERAGE_FRAMES	10	/**< Number of frames to read before doing average calculations for VBR files */
#define DO_FAST_CALC		1	/**< Whether we should read the whole file and get accurate bitrate/length or not */
#define XING_MAGIC1		( ('X' << 24) | ('i' << 16) | ('n' << 8) | 'g' )	/**< XING magic header */
#define	XING_MAGIC2		( ('I' << 24) | ('n' << 16) | ('f' << 8) | 'o' )	/**< XING magic header */

/** xing header structure */
struct xing {
	long		flags;		/* valid fields (see below) */
	unsigned long	frames;		/* total number of frames */
	unsigned long	bytes;		/* total number of bytes */
	unsigned char	toc[100];	/* 100-point seek table */
	long		scale;		/* ?? */
};

/** xing fields enumeration */
enum {
	XING_FRAMES = 0x00000001L,
	XING_BYTES  = 0x00000002L,
	XING_TOC    = 0x00000004L,
	XING_SCALE  = 0x00000008L
};

#undef DETECT_BY_CONTENT_NOT_BORKEN
#ifdef DETECT_BY_CONTENT_NOT_BORKEN
/**
 * Check file header to see if it is an MP3 file.
 * Read the header of the file in search of known magic numbers.
 * @todo Optimize.
 * @param in The file stream to check
 * @return FALSE when the header is correct, TRUE otherwise.
 */
static short mp3_content_check(FILE *const in)
{
	unsigned char		tmp[1024];
	int read, i;

	rewind(in);	/* make sure we're at the beginning of the file */

	/* Some files have plenty of useless 0x00 bytes at the beginning. The
	   magic number is still present, but after... */
	read = (int)fread(tmp, (size_t)1, (size_t)1024, in);

	for (i=0; i<read; i++) {
		if (tmp[i] != (unsigned char)0x00)
			break;
	}

	if (i >= read-2)
		return 1;

	/* Try an autodetect by hand, based on magic number */
	if ((tmp[i] == (unsigned char)0xff) && ((tmp[i+1] & (unsigned char)0xfe) == (unsigned char)0xfa))		/* MPEG Layer 2 */
		return 0;
	if ((tmp[i] == (unsigned char)0xff) && ((tmp[i+1] & (unsigned char)0xfe) == (unsigned char)0xfc))		/* MPEG Layer 3 */
		return 0;
	if ((tmp[i] == (unsigned char)'I') && (tmp[i+1] == (unsigned char)'D') && (tmp[i+2] == (unsigned char)'3')) 	/* ID3V2 */
		return 0;
	
	return 1;
}
#endif

/**
 * Parse Xing VBR headers.
 * This function comes from xmms-mad plugin.
 *
 * @param xing Xing structure to populate.
 * @param ptr mad_bitptr struct.
 * @param bitlen bit size.
 * @return 0 on success, -1 otherwise.
 */
static short xing_parse(struct xing *xing, struct mad_bitptr ptr, unsigned short bitlen)
{
	unsigned long	xing_magic;
	
	if (bitlen < 64)
		goto fail;
	
	xing_magic = mad_bit_read(&ptr, 32);
	if (xing_magic != XING_MAGIC1 && xing_magic != XING_MAGIC2)
		goto fail;

	xing->flags = mad_bit_read(&ptr, 32);
	bitlen -= 64;

	if (xing->flags & XING_FRAMES) {
		if (bitlen < 32)
			goto fail;

		xing->frames = mad_bit_read(&ptr, 32);
		bitlen -= 32;
	}

	if (xing->flags & XING_BYTES) {
		if (bitlen < 32)
			goto fail;

		xing->bytes = mad_bit_read(&ptr, 32);
		bitlen -= 32;
	}

	if (xing->flags & XING_TOC) {
		register unsigned short i;

		if (bitlen < 800)
			goto fail;

		for (i = 0; i < 100; ++i)
		xing->toc[i] = mad_bit_read(&ptr, 8);

		bitlen -= 800;
	}

	if (xing->flags & XING_SCALE) {
		if (bitlen < 32)
			goto fail;

		xing->scale = mad_bit_read(&ptr, 32);
		bitlen -= 32;
	}

	return 0;

fail:
	xing->flags = 0;
	return -1;
}

/**
 * Checks for valid MP3 filename extension.
 *
 * @param filename The filename to check.
 *
 * @return FALSE when the extension is correct, TRUE otherwise.
 */
#ifndef DETECT_BY_CONTENT_NOT_BORKEN
static inline short mpg123_mp3_ext_check(const char *const filename)
{
	const char *const ext = strrchr(filename, '.');
	if (ext	&& (!strncasecmp(ext, ".mp3", 4) || !strncasecmp(ext, ".mp2", 4)))
		return FALSE;
	return TRUE;
}
#endif

/**
 * Converts an id3tag string in utf8.
 *
 * This function reads the id3tag from an MP3 file (if any) and converts
 * the result in the utf8 standard format. It handles V1/V2 genres correctly.
 *
 * @warning Allocates memory. Don't forget to free().
 * @bug FIXME bracketed genre.
 *
 * @param tag The tag struct to parse
 * @param frameid The type of tag data searched
 * @param index The id3tag index value
 *
 * @return When possible, an utf8_t string, NULL otherwise.
 */
static id3_utf8_t *utf8_id3tag_findframe(struct id3_tag *tag, const char *const frameid,
	unsigned short index)
{
	const struct id3_frame	*frame = NULL;
	const union id3_field	*field = NULL;
	const id3_ucs4_t	*ucs4 = NULL;
	unsigned int		nstrings, i, mp3gid;

	if ((frame = id3_tag_findframe (tag, frameid, index))) {
		field = id3_frame_field(frame, 1);
		nstrings = id3_field_getnstrings(field);
		for (i = 0; i < nstrings; ++i) {
			if ((ucs4 = id3_field_getstrings(field, i))) {
				if (strcmp(frameid, ID3_FRAME_GENRE) == 0) {
					/* Some encoders encaps id nb in brackets and the
					lib doesn't handle this properly. This is ugly */
					if ((ucs4[0] == '(') && (isdigit(ucs4[1]))) {
						mp3gid = id3_ucs4_getnumber(ucs4+1);
						ucs4 = id3_genre_index(mp3gid);
					}
					else
						ucs4 = id3_genre_name(ucs4);
				}
				return(id3_ucs4_utf8duplicate(ucs4));
				/* id3_ucs4_utf8duplicate() allocates memory.
				Don't forget to free() after the function call. */
			}
		}
	}
	return NULL;
}

/**
 * Fills in the information fields about MP3 data.
 *
 * This function reads the id3 tags (using libid3tag0) from the MP3 file @a in
 * and fills in the struct mu_ent fields accordingly.
 *
 * @param pool Pool
 * @param in MP3 file to parse (closed on normal exit)
 * @param filename current filename
 * @param r Apache request_rec struct to handle log writings (debugging)
 *
 * @bug Some VBR files are reported as "bitrate: 0".
 *
 * @return When possible, struct mu_ent correctly set up, file stream closed.
 */
mu_ent *make_mp3_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
	const char *const filename)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	mu_ent 			*p = NULL;

	struct id3_file		*id3struct = NULL;
	struct id3_tag		*tag = NULL;
	id3_utf8_t 		*utf8 = NULL;

	struct stat		filestat;

	struct mad_stream 	madstream;
	struct mad_header	madheader;
	struct mad_frame	madframe;
	struct xing		xing = { .frames = 0, .bytes = 0 };
	
	unsigned char		madinput_buffer[INPUT_BUFFER_SIZE];
	size_t			madread_size, remaining;
	mad_timer_t		madduration = mad_timer_zero;	/* total play time */

	unsigned short		has_xing = 0;
	unsigned long		tagsize = 0, data_used = 0, frames = 0;
	
#ifdef DETECT_BY_CONTENT_NOT_BORKEN
	if (mp3_content_check(in))
#else
	if (mpg123_mp3_ext_check(filename))
#endif
		goto exit;
	
	fstat(fileno(in), &filestat);

	p = NEW_ENT(pool);
	if (p == NULL)
		return NULL;

	p->filetype = FT_MP3;
	p->flags &= ~EF_VBR;

	p->size = filestat.st_size;
	p->mtime = filestat.st_mtime;
	
	/* Check for an ID3 tag and read it if any */
	if ((id3struct = id3_file_open(filename, ID3_FILE_MODE_READONLY))) {
		if ((tag = id3_file_tag(id3struct)) && (tag->frames)) {
			/* tagsize = tag->paddedsize; */
			
			if ((utf8 = utf8_id3tag_findframe(tag, ID3_FRAME_TITLE, 0))) {
				p->title = apr_pstrdup(pool, (char *)utf8);
				free(utf8);
			}

			if ((utf8 = utf8_id3tag_findframe(tag, ID3_FRAME_ARTIST, 0))) {
				p->artist = apr_pstrdup(pool, (char *)utf8);
				free(utf8);
			}

			if ((utf8 = utf8_id3tag_findframe(tag, ID3_FRAME_ALBUM, 0))) {
				p->album = apr_pstrdup(pool, (char *)utf8);
				free(utf8);
			}

			if ((utf8 = utf8_id3tag_findframe(tag, ID3_FRAME_YEAR, 0))) {
				p->date = atoi((char *)utf8);
				free(utf8);
			}

			if ((utf8 = utf8_id3tag_findframe(tag, ID3_FRAME_TRACK, 0))) {
				p->track = atoi((char *)utf8);
				free(utf8);
			}
			/* TPOS is Part Of a Set (aka disc number) */
			if ((utf8 = utf8_id3tag_findframe(tag, "TPOS", 0))) {
				p->posn = atoi((char *)utf8);
				free(utf8);
			}
			/* Some MP3s (especially VBRs) are nice enough to tag their length */
			if ((utf8 = utf8_id3tag_findframe(tag, "TLEN", 0))) {
				if ((atoi((char *)utf8) / 1000) > 0)
					p->length = atoi((char *)utf8) / 1000;
				free(utf8);
			}
		
			if ((utf8 = utf8_id3tag_findframe(tag, ID3_FRAME_GENRE, 0))) {
				p->genre = apr_pstrdup(pool, (char *)utf8);
				free(utf8);
			}
		}
		id3_file_close (id3struct);
	}	/* Done with ID3 tags */
	
	/* Now it's time for mad operations to find out the bitrate, length & samplerate! */
	if (conf->options & (MI_QUICKPL))
		p->bitrate = p->length = p->freq = 0;
	else {
		/* initialisation of the MAD subsystem */
		mad_stream_init(&madstream);
		mad_header_init(&madheader);
		mad_frame_init(&madframe);

		while (1) {
			/* find out how much we have to read from the file */
			remaining = madstream.bufend - madstream.next_frame;
			memcpy(madinput_buffer, madstream.this_frame, remaining);
			madread_size = fread(madinput_buffer + remaining, 1, INPUT_BUFFER_SIZE - remaining, in);

			if (madread_size <= 0) {
				mi_rdebug("maderror madread_size <= 0 on %s", filename);
				break;
				/* TODO traitement eventuel erreur lecture */
			}

			mad_stream_buffer(&madstream, madinput_buffer, madread_size + remaining);

			while (1) {
				if (mad_header_decode(&madheader, &madstream) == -1) {
					if (madstream.error == MAD_ERROR_BUFLEN)
						break;
					if (!MAD_RECOVERABLE(madstream.error)) {
						/* TODO traitement eventuel erreur recoverable */
						mi_rdebug("maderror unrecoverable header decode on %s", filename);
						break;
					}
					if (madstream.error == MAD_ERROR_LOSTSYNC) {
						/* ignore LOSTSYNC due to ID3 tags */
	      					tagsize = id3_tag_query(madstream.this_frame, madstream.bufend - madstream.this_frame);
						if (tagsize > 0) {
							mad_stream_skip(&madstream, tagsize);
							continue;
						}
					}
					continue;
				}
			
				frames++;	/* count the number of frames for average length calculation */
				mad_timer_add(&madduration, madheader.duration);	/* sum frame duration */
				data_used += madstream.next_frame - madstream.this_frame;

				if (frames == 1) {
					/* the first frame should give us pretty much everything we need, unless... */
					p->freq = madheader.samplerate;
					p->bitrate = madheader.bitrate;
					
					/* ...we have a VBR file. See if it has XING headers first */
					madframe.header = madheader;
					if (mad_frame_decode(&madframe, &madstream) == -1) {
						if (!MAD_RECOVERABLE(madstream.error)) {
							/* TODO traitement eventuel erreur recoverable */
							mi_rdebug("maderror unrecoverable frame decode on %s", filename);
							break;
						}
					}
				
					if (xing_parse(&xing, madstream.anc_ptr, madstream.anc_bitlen) == 0) {
						/* found xing header */
						if (xing.frames) {	/* some files are broken beyond repair, ignore them */
							has_xing = 1;
							p->flags |= EF_VBR;
							/* Get the total number of frames and find out the average bitrate */
							frames = xing.frames;
							mad_timer_multiply(&madduration, frames);
							p->bitrate = 8 * xing.bytes / mad_timer_count(madduration, MAD_UNITS_SECONDS);
						}
						break;
					}
				}
				else {
					/* Maybe we have a VBR file without xing header */
					if (p->bitrate != madheader.bitrate)
						p->flags |= EF_VBR;
					if ((p->flags & EF_VBR))
						p->bitrate += madheader.bitrate;	/* we'll do an average, XXX overflow when no FAST_CALC? */
				}	/* XXX FIXME this loop will yield wrong result eg if the first
					   2+ frames have the same bitrate, the sum will only trigger
					   after these and the average performed later on on the total
					   number of frames will be incorrect. */

				/* We can either scan the whole file to get precise duration & bitrate,
				or just check a few frames and guess the whole story, if DO_FAST_CALC is set */
				if (DO_FAST_CALC && frames >= MAD_AVERAGE_FRAMES) {
					float frame_size = ((double)data_used) / MAD_AVERAGE_FRAMES;
					frames = (p->size - tagsize) / frame_size;
					
					madduration.seconds /= MAD_AVERAGE_FRAMES;
					madduration.fraction /= MAD_AVERAGE_FRAMES;
					mad_timer_multiply(&madduration, frames);
					break;
				}
			}
			
			/* We need to break the main loop either on fatal error or, if doing
			FAST_CALC, when we have already computed the numbers we need. Otherwise
			things get really messy (frames not being == to the number of frames
			actually read */
			if ((madstream.error != MAD_ERROR_BUFLEN) || (DO_FAST_CALC && (frames > MAD_AVERAGE_FRAMES)))
				break;
		}
	
		if ((p->flags & EF_VBR) && !has_xing)
			p->bitrate = p->bitrate / frames * 1000;
		if (!p->length)	/* remember, length can sometimes be encoded in ID3 tags */
			p->length = mad_timer_count(madduration, MAD_UNITS_SECONDS);

		/* MAD subsystem exit */
		mad_frame_finish(&madframe);
		mad_header_finish(&madheader);
		mad_stream_finish(&madstream);
  
		/* convert the bitrate to bytes */
		p->bitrate <<= 10;	/* * 1024 */
		p->bitrate /= 1000;	/* / 1000 - avoid FP */
	}
	
	fclose(in);

exit:
	return p;
}
