/******************************************************************************
 *
 *	tagged
 *		a simple id3-tag reading utility 
 *		this little hack is in the public domain
 *
 *		compile on systems one can work with via:
 *			gcc -Wall -std=c99 -Wall -lid3tag -o tagged tagged.c
 *		it could be helpful if you have libid3tag installed ;)
 *		find out about using it via:
 *			./tagged -h
 *
 *****************************************************************************/


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>

#include <id3tag.h>

#define __DEBUG__
#undef __DEBUG__

/**********************************
 *
 *	typedefs
 *
 *********************************/

typedef unsigned int uint;

typedef struct _Atom {
	int 		type;
	int 		processed;
	char* 		string;
}
	Atom;

typedef struct _AtomList {
	Atom**		atoms;
	uint		n_atoms;
}
	AtomList;

typedef struct _FileQueue {
	char** 		files;
	uint		n_files;
}
	FileQueue;

typedef struct _Tag {
	struct id3_file*id3file;
	struct id3_tag*	id3tag;
	char*		artist;
	char*		album;
	char*		title;
	char*		tracknumber;
}
	Tag;
	
typedef struct _Job {
	AtomList*	atom_list;
	FileQueue*	file_queue;
}
	Job;

typedef struct _Settings {
	int whitespace_substitution;
	char whitespace_substitution_char;
	int case_conversion;
	int unprintables_substitution;
	char unprintables_substitution_char;
	int tracknumber_n_digits;
	int halt_on_error;
}
	Settings;

/**********************************
 *
 *	constant data
 *
 *********************************/

#ifndef TRUE
	#define TRUE (1)
#endif
#ifndef FALSE
	#define FALSE (0)
#endif

/* identify format-tags controlling the output */
enum {
	TYPE_UNDEFINED = -1,
	TYPE_USERDEFINED = 0,
	TYPE_ARTIST,
	TYPE_ALBUM,
	TYPE_TITLE,
	TYPE_TRACKNUMBER,
	TYPE_YEAR
};

/* for case conversion setting */
enum {
	CASE_AS_IS,
	CASE_TO_LOWER,
	CASE_TO_UPPER
};

const int TRACKNUMBER_N_DIGITS_MAX = 10;
const int TRACKNUMBER_N_DIGITS_MIN = 1;

/**********************************
 *
 *	globals
 *
 *********************************/

Settings settings = {
	.whitespace_substitution = FALSE,
	.whitespace_substitution_char = '_',
	.case_conversion = CASE_AS_IS,
	.unprintables_substitution = TRUE,
	.unprintables_substitution_char = '_',
	.tracknumber_n_digits = 2,
	.halt_on_error = FALSE
};

/**********************************
 *
 *	function prototypes
 *
 *********************************/

Atom*		atom_new			(void);
void		atom_destroy		(Atom*);

AtomList*	atom_list_new		(void);
void		atom_list_destroy	(AtomList*);
void		atom_list_clear		(AtomList*);
int			atom_list_count		(AtomList*);
int			atom_list_push		(AtomList*, const Atom*);
int			atom_list_pop		(AtomList*);
Atom*		atom_list_at		(const AtomList*, uint pos);

FileQueue*	filequeue_new		(void);
void		filequeue_destroy	(FileQueue*);
int			filequeue_enqueue	(FileQueue*, char*);
char*		filequeue_dequeue	(FileQueue*);
int			filequeue_count		(FileQueue*);
char*		filequeue_at		(FileQueue*, uint);

Tag*		tag_new				(const char* filename);
void		tag_destroy			(Tag*);
char*		tag_get_artist		(Tag*);
char*		tag_get_album		(Tag*);
char*		tag_get_title		(Tag*);
char*		tag_get_tracknumber	(Tag*);
char*		tag_get_textframe	(Tag* t, const char* framename);

Job*		job_new				(void);
void		job_destroy			(Job*);
int			job_do				(Job*);

AtomList*	process_pattern		(const char*);

Tag*		tag_new				(const char*);
void		tag_destroy			(Tag*);

Job* 		parse_arguments		(int argc, char** argv);
void 		print_usage			(const char* app_name);


char*		strclone			(const char*);
char*		ucs4conv			(const id3_ucs4_t*);
char*		strtolower			(char*);
char*		strtoupper			(char*);

/**********************************
 *
 *	functions
 *
 *********************************/

/* utility functions */

#ifndef max
	#define max(a,b) ((a>b)?a:b)
#endif
#ifndef min
	#define min(a,b) ((a<b)?a:b)
#endif

char* strclone(const char* a)
{
	assert(a);

	int len = strlen(a);
	char* b = malloc(sizeof(char)*(len+1));
	strcpy(b, a);
	
	return b;
}

char* ucs4_extract_iso_8859_1(const id3_ucs4_t* a)
{
	char* b;
	int i, len;
	
	len = 0;
	
	while(a[len]) len++;
	
	b = (char*)malloc(sizeof(char)*(len+1));
	assert(b);
	
	for(i=0; i<len; i++) {
		b[i] = (a[i] > 255) ? '_' : a[i];
	}

	b[len] = '\0';
	
	return b;
}

char* ucs4_extract_utf8(const id3_ucs4_t* a) {
	char* b;
	int i, len;

	len = 0;

	while(a[len]) len++;

	b = (char*)malloc(sizeof(char)*(len+1));
	assert(b);

	for(i=0; i<len; i++) { 
		b[i] = (a[i] > 255) ? '_' : a[i];
	}

	b[len] = '\0';
	
	return b;
}

char* strtolower(char* a)
{
	char* b = a;

	assert(a);

	while(*b) {
		*b = tolower(*b);
		b++;
	}

	return a;
}

char* strtoupper(char* a)
{
	char* b = a;

	assert(a);
	
	while(*b) {
		*b = toupper(*b);
		b++;
	}

	return a;
}



/* atom - a singly processable part of the output string */

Atom* atom_new(void)
{
	Atom* a = (Atom*)malloc(sizeof(Atom));
	assert(a);
	
	a->type = TYPE_UNDEFINED;
	a->processed = FALSE;
	a->string = (char*)NULL;

	return a;
}

void atom_destroy(Atom* a)
{
	assert(a);

	if(a->string) {
		free(a->string);
	}

	free(a);
}

/* atom list */

AtomList* atom_list_new(void)
{
	AtomList* l = (AtomList*)malloc(sizeof(AtomList));
	assert(l);
	
	l->atoms = (Atom**)NULL;
	l->n_atoms = 0;

	return l;	
}

void atom_list_destroy(AtomList* l)
{
	int i, n;
	
	assert(l);

	n = l->n_atoms;
	
	for(i=0; i<n; i++) {
		atom_destroy(l->atoms[i]);
	}

	free(l);
}

int atom_list_count(AtomList* l)
{
	assert(l);
	return l->n_atoms;
}

void atom_list_clear(AtomList* l)
{
	int i, n;
	
	assert(l);

	n = l->n_atoms;

	for(i=0; i<n; i++) {
		atom_destroy(l->atoms[i]);
	}

	free(l->atoms);
	l->atoms = (Atom**)NULL;
}

int atom_list_push(AtomList* l, const Atom* a)
{
	assert(l);
	assert(a);
	
	if(l->n_atoms > 0) {
		l->atoms = (Atom**)realloc(l->atoms, (sizeof(Atom*)*(l->n_atoms+1)));
	} else {
		l->atoms = (Atom**)malloc(sizeof(Atom*));
	}
	assert(l->atoms);
	l->atoms[l->n_atoms] = (Atom*)a;
	l->n_atoms++;
	
	return TRUE;
}

int atom_list_pop(AtomList* l)
{
	assert(l);

	if(l->n_atoms> 0) {
		atom_destroy(l->atoms[l->n_atoms]);
		l->n_atoms--;
	}

	return TRUE;
}

Atom* atom_list_at(const AtomList* l, uint pos)
{
	assert(l);
	
	if(pos > l->n_atoms) {
		return (Atom*)NULL;
	}

	return l->atoms[pos];
}

/* file queue - just a queue for the filenames */

FileQueue* filequeue_new(void)
{
	FileQueue* q = (FileQueue*)malloc(sizeof(FileQueue));
	assert(q);

	q->files = (char**)NULL;
	q->n_files = 0;
	
	return q;
}

void filequeue_destroy(FileQueue* q)
{
	int i;
	
	assert(q);
	
	for(i=0; i<q->n_files; i++) {
		free(q->files[i]);
	}

	free(q);
}

int filequeue_enqueue(FileQueue* q, char* file)
{
	assert(q);
	
	if(q->n_files<1) {
		q->files = (char**)malloc(sizeof(char*));
	} else {
		q->files = (char**)realloc(q->files, sizeof(char*)*(q->n_files+1));
	}

	assert(q->files);
	q->files[q->n_files] = file;
	q->n_files++;

	return TRUE;
}

char* filequeue_dequeue(FileQueue* q)
{
	char* f;
	int i;
	
	assert(q);

	if(q->n_files == 0) {
		return (char*)NULL;
	}

	f = q->files[0];
	
	if(q->n_files == 1) {
		free(q->files);
		q->n_files = 0;
		return f;
	}
	
	for(i=0; i<(q->n_files-1); i++) {
		q->files[i] = q->files[i+1];
	}

	q->n_files--;
	q->files = (char**)realloc(q->files, sizeof(char*)*(q->n_files));
	assert(q->files);
	
	return f;
}

int filequeue_count(FileQueue* q)
{
	assert(q);
	return q->n_files;
}

char* filequeue_at(FileQueue* q, uint pos)
{
	assert(q);

	if(pos >= q->n_files) {
		return (char*)NULL;
	}

	return q->files[pos];
}
	
/* job - maintain requests and process them */

Job* job_new(void)
{
	Job* j = (Job*)malloc(sizeof(Job));
	assert(j);
	
	j->atom_list = (AtomList*)NULL;
	j->file_queue = (FileQueue*)NULL;

	return j;
}

void job_destroy(Job* j)
{
	assert(j);

	if(j->file_queue) {
		filequeue_destroy(j->file_queue);
	}
	
	if(j->atom_list) {
		atom_list_destroy(j->atom_list);
	}

	free(j);
}

int job_do(Job* j)
{
	Tag* tag = (Tag*)NULL;
	Atom* atom = (Atom*)NULL;
	char* file = (char*)NULL;
	char* out = (char*)NULL;
	int i, n_files, n_atoms, out_len, string_len, num;

	n_files = filequeue_count(j->file_queue);
	n_atoms = atom_list_count(j->atom_list);
	out = (char*)malloc(sizeof(char));
	out[0] = '\0';
	out_len = 0;
	
	while((file = filequeue_dequeue(j->file_queue))) 
	{
		tag = tag_new(file);
	
		if(!tag) {
			free(file);

			if(settings.halt_on_error) {
				return FALSE;
			} else {
				continue;
			}
		}
		
		/* get needed tags */
		
		for(i=0;i<n_atoms;i++) {
			atom = atom_list_at(j->atom_list, i);

			switch(atom->type)
			{
				case TYPE_USERDEFINED:
					break;
				case TYPE_ARTIST:
					atom->string = tag_get_artist(tag);
					break;
				case TYPE_ALBUM:
					atom->string = tag_get_album(tag);
					break;
				case TYPE_TITLE:
					atom->string = tag_get_title(tag);
					break;
				case TYPE_TRACKNUMBER:
					atom->string = tag_get_tracknumber(tag);
					break;
				default:
					atom->string = (char*)NULL;
					break;
			}

			if(!atom->string) atom->string = strclone("");
		}

		/* process tags */

		for(i=0; i<n_atoms; i++) {
			atom = atom_list_at(j->atom_list, i);

			switch(atom->type) 
			{
				case TYPE_TRACKNUMBER:
				{
					int n, k;
					div_t temp;

					n = settings.tracknumber_n_digits;
					num = atoi(atom->string);
					atom->string = realloc(atom->string, sizeof(char)*(n+1));
					assert(atom->string);
					
					atom->string[n] = '\0';
					
					for(k=(n-1); k>=0; k--) {
						temp = div(num, 10);
						atom->string[k] = temp.rem + '0';
						num = temp.quot;
					}
					
					break;
				}
				default:
					break;
			}
		}
		
		/* make output string */

		for(i=0; i<n_atoms; i++) {
			atom = atom_list_at(j->atom_list, i);

			string_len = strlen(atom->string);

			if(string_len == 0) continue;

			out = realloc(out, sizeof(char)*(out_len + string_len + 1));
			assert(out);
			out_len += string_len;
			out = strcat(out, atom->string);
			assert(out);
		}

		/* post process output string */

		if(settings.case_conversion != CASE_AS_IS) {
			switch(settings.case_conversion) 
			{
				case CASE_TO_UPPER:
					out = strtoupper(out);
					break;
				case CASE_TO_LOWER:
					out = strtolower(out);
					break;
				default:
					assert(FALSE);
			}
		}
		
		if(settings.whitespace_substitution) {
			for(i=0; i<out_len; i++) {
				if(out[i] == ' ') { 
					out[i] = settings.whitespace_substitution_char;
				}
			}
		}
	
		/* print result */

		printf("%s\n", out);
		
		/* clean up */
		
		for(i=0;i<n_atoms;i++) {
			atom = atom_list_at(j->atom_list, i);
			
			if(atom->type != TYPE_USERDEFINED) {
				free(atom->string);
				atom->string = (char*)NULL;
			}
		}
		
		out = realloc(out, sizeof(char));
		out[0] = '\0';
		out_len = 0;
		tag_destroy(tag);
		tag = (Tag*)NULL;
		free(file);
	} /* while((file = filequeue_dequeue(j->file_queue))) */

	free(out);
	
	return TRUE;
}

/* libid3tag-wrappers */

Tag* tag_new(const char* file)
{
	Tag* t = (Tag*)malloc(sizeof(Tag));
	assert(t);

	t->id3file = id3_file_open(file, ID3_FILE_MODE_READONLY);
	if(!t->id3file) {
		fprintf(stderr, "could not open file '%s'\n", file);
		free(t);
		return (Tag*)NULL;
	}
	
	t->id3tag = id3_file_tag(t->id3file);
	if(!t->id3tag) {
		fprintf(stderr, "could not get id3-tag for file '%s'\n", file);
		id3_file_close(t->id3file);
		free(t);
		return (Tag*)NULL;
	}

	t->artist = (char*)NULL;
	t->album = (char*)NULL;
	t->title = (char*)NULL;
	t->tracknumber = (char*)NULL;

	return t;
}

void tag_destroy(Tag* t)
{
	assert(t);
	
	id3_tag_delete(t->id3tag);
	id3_file_close(t->id3file);

	if(t->artist) free(t->artist);
	if(t->album) free(t->album);
	if(t->title) free(t->title);
	if(t->tracknumber) free(t->tracknumber);
	
	free(t);
}

char* tag_get_artist(Tag* t)
{
	assert(t);
	
	if(!t->artist) t->artist = tag_get_textframe(t, "TPE1");
	assert(t->artist);
	return strclone(t->artist);
}

char* tag_get_album(Tag* t)
{
	assert(t);
	
	if(!t->album) t->album = tag_get_textframe(t, "TALB");
	assert(t->album);
	return strclone(t->album);
}

char* tag_get_title(Tag* t)
{
	assert(t);

	if(!t->title) t->title = tag_get_textframe(t, "TIT2");
	assert(t->title);
	return strclone(t->title);
}

char* tag_get_tracknumber(Tag* t)
{
	assert(t);

	if(!t->tracknumber) t->tracknumber = tag_get_textframe(t, "TRCK");
	assert(t->tracknumber);
	return strclone(t->tracknumber);
}

char* tag_get_textframe(Tag* t, const char* framename)
{
	struct id3_frame* id3frame = NULL;
	union id3_field* id3field = NULL;
	char* s;
	int encoding, id3fieldtype;
       
	assert(t);
	
	s = (char*)NULL;
	id3frame = id3_tag_findframe(t->id3tag, framename, 0);

	if(id3frame) {
		/* get encoding */
		id3field = id3_frame_field(id3frame, 0);
		id3fieldtype = id3_field_type(id3field);
		assert(id3fieldtype == ID3_FIELD_TYPE_TEXTENCODING);
		encoding = id3_field_gettextencoding(id3field);
                
		switch(encoding) {
			case ID3_FIELD_TEXTENCODING_ISO_8859_1:
				id3field = id3_frame_field(id3frame, 1);
	 			assert(id3field);
				s = ucs4_extract_iso_8859_1(id3_field_getstrings(id3field, 0));
				id3_frame_delete(id3frame);
				break;
			case ID3_FIELD_TEXTENCODING_UTF_8:
			{
				
				id3field = id3_frame_field(id3frame, 1);
				assert(id3field);
				s = ucs4_extract_utf8(id3_field_getstrings(id3field, 0));
				id3_frame_delete(id3frame);
				break;
			}
		}
		
	}

	return s;
}

/* process pattern and build atom-list */

AtomList* process_pattern(const char* pattern)
{
	typedef struct {
		char name;
		int type;
	}
		Tag;
	
	AtomList* alist;
	Atom* atom;
	Tag tags[] = {{'a', TYPE_ARTIST}, {'A', TYPE_ALBUM}, {'t', TYPE_TITLE}, {'T', TYPE_TRACKNUMBER}};
	int n_tags = 4;
	int pattern_len;
	int first, sub_len;
	int n_atoms;
	int i, j;
		
	assert(pattern);
	
	alist = atom_list_new();
	pattern_len = strlen(pattern);
	first = 0;
	
	for(i=0; i<pattern_len; i++) {
		if(pattern[i]=='%' && i<(pattern_len-1)) {
			if((i==0) || (pattern[i-1] != '\\')) {
				if(pattern[i+1] == '%') {
					i = i+1;
					continue;
				}
				
				/* save preceding defined chars */
				if(i>0) {
					sub_len = i - first;
					atom = atom_new();
					atom->type = TYPE_USERDEFINED;
					atom->string = (char*)malloc(sizeof(char)*(sub_len+1));
					strncpy(atom->string, pattern+first, sub_len);
					atom->string[sub_len] = '\0';
					atom_list_push(alist, atom);
				}
				
				/* save the char defining desired tag */
				atom = atom_new();
				atom->string = (char*)malloc(sizeof(char)*2);
				atom->string[0] = pattern[i+1];
				atom->string[1] = '\0';
				atom_list_push(alist, atom);

				first = i+2;
			}
		}
	}
	
	/* save trailing user defined chars */
	if(first<pattern_len) {
		sub_len = pattern_len - first;
		atom = atom_new();
		atom->type = TYPE_USERDEFINED;
		atom->string = (char*)malloc(sizeof(char)*(sub_len+1));
		strncpy(atom->string, pattern+first, (size_t)sub_len);
		atom->string[sub_len] = '\0';
		atom_list_push(alist, atom);
	}
	
	/* identify tags */
	n_atoms = atom_list_count(alist);
	
	for(i=0; i<n_atoms; i++) {
		atom = atom_list_at(alist, i);
		
		if(atom->type == TYPE_USERDEFINED) continue;
		
		for(j=0; j<n_tags; j++) {
			if(tags[j].name == atom->string[0]) {
				atom->type = tags[j].type;
				free(atom->string);
				atom->string = (char*)NULL;
				break;
			}
		}
	}

	#ifdef __DEBUG__
        for(i=0; i<atom_list_count(alist); i++) {
		atom = atom_list_at(alist,i);
		printf("DEBUG: atomList[%d]->type: %d\n",i,atom->type);
		printf("DEBUG: atomList[%d]->string: '%s'\n", i, (atom->string)?atom->string:"<null>");
	}
	#endif
	
	return alist;
}

/* standard stuff */

void print_usage(const char* app_name)
{
	printf("tagged - a simple tool for formatted output of id3v2-tags.\n"
			" this app is in the public domain.\n\n");
	printf("usage: %s [-hnwWcC] <pattern> <file> [<files>]\n", app_name);
	printf(	"\tw: substitute whitespace with '_'\n"
			"\tW <char>: substitute whitespace with <char>\n"
			"\tc: convert output to lower case\n"
			"\tC: convert output to upper case\n"
			"\tn <n>: use n digits for tracknumber\n"
			"\th: print this lame message\n"
			"\n"
			"<pattern> defines what output you get and how it\n"
			"will be formatted.\n"
			"you may use '%%a' for artist, '%%A' for album,\n"
			"'%%T' for tracknumber, '%%t' for title\n"
			"and '%%%%' to escape '%%'.\n"
			"thus for example \n"
			"\t'./tagged -wC \"%%T - %%a - %%t\" foo.mp3'\n"
			"could produce the output:\n"
			"\t'05_-_Your_Artist_-_Your_-_Title'\n");
}

Job* parse_arguments(int argc, char** argv)
{
	Job* job = (Job*)NULL;
	AtomList* alist = (AtomList*)NULL;
	FileQueue* fqueue = (FileQueue*)NULL;
	char *arg = (char*)NULL, 
	     *pattern = (char*)NULL, 
	     *file = (char*)NULL;
	int i, j, len, cont, n;

	if(argc<3) {
		print_usage(argv[0]);
		goto _label_error;
	}
	
	/* parse 'em */
	
	fqueue = filequeue_new();
	
	for(i=1; i<argc; i++) {
		arg = argv[i];
		if(!arg[0]) continue;
		
		/* options first ... */		
		if(arg[0] == '-') {
			if(pattern) {
				print_usage(argv[0]);
				goto _label_error;
			}

			cont = TRUE;
			
			for(j=1; arg[j] && cont; j++) {
				switch(arg[j])
				{

					/* substitute whitespace by default char */
					case 'w':
						settings.whitespace_substitution = TRUE;
						break;
					
					/* substitute whitespace by user-defined subst-char */
					case 'W':
						if((i==(argc-1)) || (arg[j+1] != '\0')) {
							fprintf(stderr, "option W expects following character\n");
							goto _label_error;
						}
					
						i++;
						arg = argv[i];
						cont = FALSE;
						
						if(strlen(arg) != 1) {
							fprintf(stderr, "option W expects a character\n");
							goto _label_error;
						}
												
						settings.whitespace_substitution = TRUE;
						settings.whitespace_substitution_char = arg[0];
						break;
					
					/* all upper case */
					case 'C':
						settings.case_conversion = CASE_TO_UPPER;
						break;
					
					/* all lower case */
					case 'c':
						settings.case_conversion = CASE_TO_LOWER;
						break;
					
					/* use n digits for tracknumber */
					case 'n':
						if((i==(argc-1)) || (arg[j+1] != '\0')) {
							fprintf(stderr, "option n expects following number\n");
                            goto _label_error;
						}
						
						i++;
						arg = argv[i];
						cont = FALSE;

						if(strlen(arg) < 1) {
							fprintf(stderr, "option n expects following number\n");
							goto _label_error;
						}
						
						n = atoi(arg);
						n = min(max(n, TRACKNUMBER_N_DIGITS_MIN), TRACKNUMBER_N_DIGITS_MAX);
						settings.tracknumber_n_digits = n;
						break;
						
					/* dont panic */
					case 'h':
						print_usage(argv[0]);
						break;
					default:
						fprintf(stderr, "unknown option %c\n", arg[j]);
						goto _label_error;
				}
			}
		/* ... then pattern ... */
		} else if(!pattern) {
			len = strlen(arg);
			pattern = (char*)malloc(sizeof(char)*(len+1));
			strcpy(pattern, arg);
		/* ... then files */
		} else {
			file = (char*)malloc(sizeof(char)*(strlen(arg)+1));
			strcpy(file, arg);
			filequeue_enqueue(fqueue, file);
		}
	}

	/* feed the job */
	
	job = job_new();
	job->file_queue = fqueue;
	
	#ifdef __DEBUG__
	for(i=0; i<filequeue_count(fqueue); i++) {
		file = filequeue_at(fqueue, i);
		printf("DEBUG: fileQueue[%d]: %s\n", i, file);
	}
	#endif
	
	alist = process_pattern(pattern);

	if(!alist) goto _label_error;
	
	job->atom_list = alist;
	
	return job;

	_label_error:
	if(alist) atom_list_destroy(alist);
	if(fqueue) filequeue_destroy(fqueue);
	if(job) job_destroy(job);
	return (Job*)NULL;
}

int main(int argc, char** argv)
{
	Job* job;
	int state = 0;
	
	job = parse_arguments(argc, argv);

	if(!job) {
		return 1;
	}

	if(job_do(job) != TRUE) {
		state = 1;
	}
	
	job_destroy(job);
	
	return state;
}

