/* id3 - an ID3 tag editor
 * Copyright (c) 1998 - 2006  Robert Woodcock <rcw@debian.org>
 * Copyright (c) 2016, 2018  Peter Pentchev <roam@ringlet.net>
 * This code is hereby licensed for public consumption under either the
 * GNU GPL v2 or greater, or Larry Wall's Artistic license - your choice.
 *
 * 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.
 *                                 
 * genre.h is (c) 1998 Robert Alto <badcrc@tscnet.com> and licensed only
 * under the GPL.
 */

#include <sys/types.h>
#include <sys/stat.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#include "genre.h"

static const char version[]="1.1.0";

static const char usage[]= "usage: id3 -[tTaAycg] \"value\" file1 [file2...]\n\
       id3 -d file1 [file2...]\n\
       id3 -l file1 [file2...]\n\
       id3 -L\n\
       id3 -v\n\
 -t   Modifies a Title tag\n\
 -T   Modifies a Track tag\n\
 -a   Modifies an Artist tag\n\
 -A   Modifies an Album tag\n\
 -y   Modifies a Year tag\n\
 -c   Modifies a Comment tag\n\
 -g   Modifies a Genre tag\n\
 -l   Lists an ID3 tag\n\
 -L   Lists all genres\n\
 -R   Uses an rfc822-style format for output\n\
 -d   Deletes an ID3 tag\n\
 -h   Displays this help info\n\
 -v   Prints version info\n";

struct id3 {
	char tag[3];
	char title[30];
	char artist[30];
	char album[30];
	char year[4];
	/* With ID3 v1.0, the comment is 30 chars long */
	/* With ID3 v1.1, if comment[28] == 0 then comment[29] == tracknum */
	char comment[30];
	unsigned char genre;
};

static void
show_tag(const struct id3 * const id3, const char * const fname, const bool rfc822style, const bool v11tag)
{
	if (rfc822style) {
		printf("\nFilename: %s\n", fname);
		printf("Title: %-30.30s\n", id3->title);
		printf("Artist: %-30.30s\n", id3->artist);
		printf("Album: %-30.30s\n", id3->album);
		printf("Year: %-4.4s\n", id3->year);
		printf("Genre: %s (%d)\n",
			(id3->genre < genre_count) ?
			genre_table[id3->genre] : "Unknown", id3->genre);
		if (v11tag)
			printf("Track: %u\nComment: %-28.28s\n",
				(unsigned char)id3->comment[29], id3->comment);
		else
			printf("Comment: %-30.30s\n", id3->comment);	
	} else {
		if (fname != NULL)
			printf("%s:\n", fname);
		printf("Title  : %-30.30s  Artist: %-30.30s\n",
			id3->title, id3->artist);
		printf("Album  : %-30.30s  Year: %-4.4s, Genre: %s (%d)\n",
			id3->album, id3->year, 
			(id3->genre < genre_count)
			? genre_table[id3->genre] : 
			"Unknown", id3->genre);
			if (v11tag)
				printf("Comment: %-28.28s    Track: %u\n", id3->comment, (unsigned char)id3->comment[29]);
			else
				printf("Comment: %-30.30s\n", id3->comment);
	}
}

static void
zcopy(char * const dst, const char * const src, const size_t size)
{
	const size_t len = strlen(src);
	const size_t cp = len < size? len: size;
	memcpy(dst, src, cp);
	if (cp < size)
		memset(dst + cp, 0, size - cp);
}

static unsigned long long
strtoull_full(const char * const s, bool * const convErr, const int base, const char delim, const unsigned long long limit)
{
	if (!( (*s >= '0' && *s <= '9') ||
		   (*s >= 'A' && *s <= 'Z') ||
		   (*s >= 'a' && *s <= 'z') )) {
		*convErr = true;
		return 0;
	}

	errno = 0;
	char *end;
	const unsigned long long val = strtoull( s, &end, base );
	if( *end != delim || (val == ULLONG_MAX && errno == ERANGE) ||
	    (limit != 0 && val > limit) ) {
		*convErr = true;
		return 0;
	}

	*convErr = false;
	return val;
}

static inline unsigned long long
strtoull_limit(const char * const s, bool * const convErr, const unsigned long long limit)
{
	return strtoull_full(s, convErr, 10, '\0', limit);
}

int main (int argc, char *argv[])
{
	bool showhelp=false, showversion=false, showfeatures=false;
	bool deletetag=false, listtag=false, rfc822style=false;
	int exitcode=0;
	bool newtitle=false, newartist=false, newalbum=false;
	bool newyear=false, newcomment=false, newgenre=false;
	bool newtrack=false;
	struct id3 newid3;
	
	memset(&newid3, 0, 127);
	newid3.genre=255;
	
	if (argc < 2) {
		fprintf(stderr, "%s", usage);
		return 1;
	}
	
	while (true) {
		const int r = getopt(argc, argv, "dhlLRvt:T:a:A:y:c:g:-:");
		if (r == -1) break;
		switch (r) {
		case 'd': /* delete a tag */
			deletetag=true;
			break;
		case 'l': /* list a tag */
			listtag=true;
			break;
		case 'L': /* list all genres */
		        for (unsigned i=0; i<genre_count; i++)
		        	printf("%3d: %s\n", i, genre_table[i]);
		        return 0;
		case 'R': /* list tags in rfc822-style format */
		        rfc822style=true;
		        break;
		case 't': /* Title */
			zcopy(newid3.title, optarg, 30);
			newtitle=true;
			break;
		case 'T': /* Track */
			{
			newid3.comment[28] = 0;
			bool convErr;
			newid3.comment[29] = (unsigned char)strtoull_limit(optarg, &convErr, 255);
			if (convErr)
				errx(1, "Track: '%s': Expected a number between 0 and 255.", optarg);
			newtrack=true;
			}
			break;
		case 'a': /* Artist */
			zcopy(newid3.artist, optarg, 30);
			newartist=true;
			break;
		case 'A': /* Album */
			zcopy(newid3.album, optarg, 30);
			newalbum=true;
			break;
		case 'y': /* Year */
			zcopy(newid3.year, optarg, 4);
			newyear=true;
			break;
		case 'c': /* Comment */
			zcopy(newid3.comment, optarg, 28);
			newcomment=true;
			break;
		case 'g': /* Genre */
			if (isdigit(optarg[0])) { /* genre by number */
				bool convErr;
				newid3.genre = (unsigned char)strtoull_limit(optarg, &convErr, 255);
				if (convErr)
					errx(1, "Genre: Invalid number '%s'.", optarg);
			} else { /* genre by name */
				unsigned matches=3; /* don't trip the first time */
				/* lazy match - keep comparing down the list until we only get one hit */
				for (unsigned lettersmatched=0; matches>1; lettersmatched++) {
					for (unsigned i=matches=0; i<genre_count; i++) {
						/* Kludge to avoid infinite loop with "-g Fusion" */
						if (i == 84) i++;
						if (strncasecmp(genre_table[i], optarg, lettersmatched) == 0) {
							matches++;
							newid3.genre = (unsigned char)i;
						}
					}
				}
				if (matches == 0) /* no hits - complain to user */
					err(1, "No such genre '%s'.", optarg);
			}	
			newgenre=true;
			break;
		case 'v': /* Version info */
			showversion = true;
			break;
		case 'h': /* Help info */
			showhelp=true;
			break;
		case '-':
			if (strcmp(optarg, "help") == 0) {
				showhelp = true;
				break;
			} else if (strcmp(optarg, "version") == 0) {
				showversion = true;
				break;
			} else if (strcmp(optarg, "features") == 0) {
				showfeatures = true;
				break;
			}
			/* fallthrough */
		case ':': /* need a value for an option */
		case '?': /* unknown option */
			fprintf(stderr, "%s", usage);
			return 1;
		}
	}
	if (showversion)
		printf("This is id3 v%s.\n", version);
	if (showhelp)
		printf("%s", usage);
	if (showfeatures)
		printf("Features: id3=%s\n", version);
	if (showversion || showhelp || showfeatures)
		return 0;
	
	if (optind >= argc)
		err(1, "Need a filename to work on.");
	
	for (int i=optind; i<argc; i++) {
		const bool rw = newtitle || newartist || newalbum || newyear ||
			newcomment || newtrack || newgenre || deletetag;
		FILE * const fp = fopen(argv[i], rw? "r+": "r");
		if (fp == NULL) { /* file didn't open */
			warn("fopen: %s", argv[i]);
			exitcode=1;
			continue;
		}
		struct stat finfo;
		if (fstat(fileno(fp), &finfo) == -1) {
			warn("fstat: %s", argv[i]);
			exitcode=1;
			continue;
		}
		const int notdir = !S_ISDIR(finfo.st_mode);
		
		bool hastag = true;
		struct id3 oldid3;
		if (fseek(fp, -128, SEEK_END) == -1) {
			/* problem rewinding */
			hastag=false;
		} else { /* we rewound successfully */ 
			if (fread(&oldid3, 128, 1, fp) < 1) {
				/* read error */
				warn("fread: %s", argv[i]);
				exitcode=1;
				hastag=false;
			}
		}
		
		/* This simple detection code has a 1 in 16777216
		 * chance of misrecognizing or deleting the last 128
		 * bytes of your mp3 if it isn't tagged. ID3 ain't
		 * world peace, live with it.
		 */
		
		if (hastag && strncmp(oldid3.tag, "TAG", 3) != 0)
			hastag=false;
		
		/* v1.1 tag == true if comment[28] == 0 */
		const int v11tag = hastag && oldid3.comment[28] == '\0';
		
		if (!hastag) {
			memset(&oldid3, 0, 127);
			oldid3.genre=255;
		}

		if (listtag) {
			if (hastag || rfc822style)
				show_tag(&oldid3, argv[i], rfc822style, v11tag);
			else
				printf("%s: %s\n", argv[i],
				    notdir? "No ID3 tag": "Directory");
		}
		
		if (hastag && deletetag) {
			if (ftruncate(fileno(fp), ftell(fp)-128) == -1) {
				warn("ftruncate: %s", argv[i]);
				exitcode=1;
			}
			fclose(fp);
			continue;
		}
		
		strncpy(newid3.tag, "TAG", 3);
		if (!newtitle) memcpy(&newid3.title, &oldid3.title, 30);		
		if (!newartist) memcpy(&newid3.artist, &oldid3.artist, 30);
		if (!newalbum) memcpy(&newid3.album, &oldid3.album, 30);
		if (!newyear) memcpy(&newid3.year, &oldid3.year, 4);
		if (!newcomment) memcpy(&newid3.comment, &oldid3.comment, 28);
		if (!newtrack) { memcpy(&newid3.comment[28], &oldid3.comment[28], 2); }
		if (!newgenre) memcpy(&newid3.genre, &oldid3.genre, 1);


		if (newtitle || newartist || newalbum || newyear || newcomment || newgenre || newtrack) {
			/* something did change */
			const bool new_v11tag = !newid3.comment[28];
			show_tag(&newid3, rfc822style? argv[i]: NULL, rfc822style, new_v11tag);
			if (hastag) {
				/* seek back 128 bytes to overwrite it */
				fseek(fp, -128, SEEK_END);
			} else {
				/* new tag */
				fseek(fp, 0, SEEK_END);
			}
			if (fwrite(&newid3, 128, 1, fp) < 1) {
				warn("fwrite: %s", argv[i]);
				exitcode=1;
			}
		}
		if (fclose(fp) == EOF) {
			warn("fclose: %s", argv[i]);
			exitcode=1;
		}
		
	}
	return exitcode;
}
