/*
 * cueprint.c -- print cd information based on a template
 *
 * Copyright (C) 2004, 2005, 2006, 2007, 2013 Svend Sorensen
 * For license terms, see the file COPYING in this distribution.
 */

#include <ctype.h>	/* isdigit() */
#include <getopt.h>	/* getopt_long() */
#include <stdio.h>	/* fprintf(), printf(), snprintf(), stderr */
#include <stdlib.h>	/* exit() */
#include <string.h>	/* strcasecmp() */
#include "cuefile.h"

#if HAVE_CONFIG_H
#include "config.h"
#else /* not HAVE_CONFIG_H */
#define PACKAGE_STRING "cueprint"
#endif /* HAVE_CONFIG_H */

/* default templates */

#define D_TEMPLATE "\
Disc Information\n\
arranger:	%A\n\
composer:	%C\n\
genre:		%G\n\
message:	%M\n\
no. of tracks:	%N\n\
performer:	%P\n\
songwriter:	%S\n\
title:		%T\n\
UPC/EAN:	%U\n\
"

#define T_TEMPLATE "\
\n\
Track %n Information\n\
arranger:	%a\n\
composer:	%c\n\
genre:		%g\n\
ISRC:		%i\n\
message:	%m\n\
track number:	%n\n\
perfomer:	%p\n\
title:		%t\n\
ISRC (CD-TEXT):	%u\n\
"

/* default string to print for unset (NULL) values */
#define VALUE_UNSET ""

/*
 * *_get_* functions can return an int or a char *
 */
typedef union {
	int ival;
	char *sval;
	char cval;
} Value;

char *progname;

/* Print usage information and exit */
void usage(int status)
{
	if (0 == status) {
		printf("Usage: %s [option...] [file...]\n", progname);
		printf("Report disc and track information from a CUE or TOC file.\n"
		       "\n"
		       "OPTIONS\n"
		       "-h, --help			print usage\n"
		       "-i, --input-format cue|toc	set format of file(s)\n"
		       "-n, --track-number <number>	only print track information for single track\n"
		       "-d, --disc-template <template>	set disc template\n"
		       "-t, --track-template <template>	set track template\n"
		       "-V, --version			print version information\n"
		       "\n"
		       "Default disc template: %s\n"
		       "Default track template: %s\n"
		       "See the manual page for more information.\n",
		       D_TEMPLATE, T_TEMPLATE);
	} else {
		fprintf(stderr, "Try `%s --help' for more information.\n", progname);
	}

	exit (status);
}

/* Print version information and exit */
void version()
{
	printf("%s\n", PACKAGE_STRING);

	exit(0);
}

/*
 * TODO: Shouldn't we be using vprintf() to help us out with this stuff?
 * (Branden Robinson)
 */
void disc_field(char *conv, int length, Cd *cd, Value *value)
{
	char *c;	/* pointer to conversion character */

	Cdtext *cdtext = NULL;
	cdtext = cd_get_cdtext(cd);

	c = conv + length - 1;

	/*
	 * After setting value, set *c to specify value type:
	 * 'd' integer
	 * 's' string
	 * 'c' character
	 */
	switch (*c) {
	case 'A':
		value->sval = cdtext_get(PTI_ARRANGER, cdtext);
		*c = 's';
		break;
	case 'C':
		value->sval = cdtext_get(PTI_COMPOSER, cdtext);
		*c = 's';
		break;
	case 'G':
		value->sval = cdtext_get(PTI_GENRE, cdtext);
		*c = 's';
		break;
	case 'M':
		value->sval = cdtext_get(PTI_MESSAGE, cdtext);
		*c = 's';
		break;
	case 'N':
		value->ival = cd_get_ntrack(cd);
		*c = 'd';
		break;
	case 'P':
		value->sval = cdtext_get(PTI_PERFORMER, cdtext);
		*c = 's';
		break;
	case 'R':
		value->sval = cdtext_get(PTI_ARRANGER, cdtext);
		*c = 's';
		break;
	case 'S':
		value->sval = cdtext_get(PTI_SONGWRITER, cdtext);
		*c = 's';
		break;
	case 'T':
		value->sval = cdtext_get(PTI_TITLE, cdtext);
		*c = 's';
		break;
	case 'U':
		value->sval = cdtext_get(PTI_UPC_ISRC, cdtext);
		*c = 's';
		break;
	default:
		value->cval = *c;
		*c = 'c';
		break;
	}
}

void track_field(char *conv, int length, Cd *cd, int trackno, Value *value)
{
	char *c;	/* pointer to conversion character */

	Track *track = NULL;
	Cdtext *cdtext = NULL;

	track = cd_get_track(cd, trackno);
	cdtext = track_get_cdtext(track);

	c = conv + length - 1;

	switch (*c) {
	case 'a':
		value->sval = cdtext_get(PTI_ARRANGER, cdtext);
		*c = 's';
		break;
	case 'c':
		value->sval = cdtext_get(PTI_COMPOSER, cdtext);
		*c = 's';
		break;
	case 'f':
		value->sval = track_get_filename(track);
		*c = 's';
		break;
	case 'g':
		value->sval = cdtext_get(PTI_GENRE, cdtext);
		*c = 's';
		break;
	case 'i':
		value->sval = track_get_isrc(track);
		*c = 's';
		break;
	case 'm':
		value->sval = cdtext_get(PTI_MESSAGE, cdtext);
		*c = 's';
		break;
	case 'n':
		value->ival = trackno;
		*c = 'd';
		break;
	case 'p':
		value->sval = cdtext_get(PTI_PERFORMER, cdtext);
		*c = 's';
		break;
	case 's':
		value->sval = cdtext_get(PTI_SONGWRITER, cdtext);
		*c = 's';
		break;
	case 't':
		value->sval = cdtext_get(PTI_TITLE, cdtext);
		*c = 's';
		break;
	case 'u':
		value->sval = cdtext_get(PTI_UPC_ISRC, cdtext);
		*c = 's';
		break;
	default:
		disc_field(conv, length, cd, value);
		break;
	}

}

/*
 * Print a conversion specification.
 * [flag(s)][width][.precision]<conversion-char>
 */
void print_conv(char *start, int length, Cd *cd, int trackno)
{
	char *conv;	/* copy of conversion specification */
	Value value;
	char *c;	/* pointer to conversion-char */

	/* TODO: use strndup? */
	conv = malloc((unsigned) (length + 1));
	strncpy(conv, start, length);
	conv[length] = '\0';

	/* conversion character */
	if (0 == trackno) {
		disc_field(conv, length, cd, &value);
	} else {
		track_field(conv, length, cd, trackno, &value);
	}

	c = conv + length - 1;

	switch (*c) {
	case 'c':
		printf(conv, value.cval);
		break;
	case 'd':
		printf(conv, value.ival);
		break;
	case 's':
		if (NULL == value.sval)
			printf(conv, VALUE_UNSET);
		else
			printf(conv, value.sval);
		break;
	default:
		printf("%zu: ", strlen(conv));
		printf("%s", conv);
	}

	free(conv);
}

void cd_printf(char *format, Cd *cd, int trackno)
{
	char *c;	/* pointer into format */
	char *conv_start;
	int conv_length;

	for (c = format; '\0' != *c; c++) {
		if ('%' == *c) {
			conv_start = c;
			conv_length = 1;
			c++;

			/* flags */
			while ( \
				'-' == *c \
				|| '+' == *c \
				|| ' ' == *c \
				|| '0' == *c \
				|| '#' == *c \
			) {
				conv_length++;
				c++;
			}

			/* field width */
			/* '*' not recognized */
			while (0 != isdigit(*c)) {
				conv_length++;
				c++;
			}

			/* precision */
			/* '*' not recognized */
			if ('.' == *c) {
				conv_length++;
				c++;

				while (0 != isdigit(*c)) {
					conv_length++;
					c++;
				}
			}

			/* length modifier (h, l, or L) */
			/* not recognized */

			/* conversion character */
			conv_length++;

			print_conv(conv_start, conv_length, cd, trackno);
		} else {
			putchar(*c);
		}
	}
}

int info(char *name, int format, int trackno, char *d_template, char *t_template)
{
	Cd *cd = NULL;
	int ntrack;

	if (NULL == (cd = cf_parse(name, &format))) {
		fprintf(stderr, "%s: error: unable to parse input file"
		        " `%s'\n", progname, name);
		return -1;
	}

	ntrack = cd_get_ntrack(cd);

	if (-1 == trackno) {
		cd_printf(d_template, cd, 0);

		for (trackno = 1; trackno <= ntrack; trackno++) {
			cd_printf(t_template, cd, trackno);
		}
	} else if (0 == trackno) {
		cd_printf(d_template, cd, trackno);
	} else if (0 < trackno && ntrack >= trackno) {
		cd_printf(t_template, cd, trackno);
	} else {
		fprintf(stderr, "%s: error: track number out of range\n", progname);
		return -1;
	}

	return 0;
}

/* 
 * Translate escape sequences in a string.
 * The string is overwritten and terminated.
 * TODO: this does not handle octal and hexidecimal escapes
 *       except for \0
 */
void translate_escapes(char *s)
{
	char *read;
	char *write;

	read = s;
	write = s;

	while ('\0' != *read) {
		if ('\\' == *read) {
			read++;

			switch (*read) {
			case 'a':
				*write = '\a';
				break;
			case 'b':
				*write = '\b';
				break;
			case 'f':
				*write = '\f';
				break;
			case 'n':
				*write = '\n';
				break;
			case 'r':
				*write = '\r';
				break;
			case 't':
				*write = '\t';
				break;
			case 'v':
				*write = '\v';
				break;
			case '0':
				*write = '\0';
				break;
			default:
				/* ?, ', " are handled by the default */
				*write = *read;
				break;
			}
		} else {
			*write = *read;
		}

		read++;
		write++;
	}

	*write = '\0';
}

int main(int argc, char *argv[])
{
	int format = UNKNOWN;
	int trackno = -1;		/* track number (-1 = unspecified,
					                  0 = disc info) */
	char *d_template = NULL;	/* disc template */
	char *t_template = NULL;	/* track template */
	int ret = 0;			/* return value of info() */

	/* option variables */
	int c;
	/* getopt_long() variables */
	extern char *optarg;
	extern int optind;

	static struct option longopts[] = {
		{"help", no_argument, NULL, 'h'},
		{"input-format", required_argument, NULL, 'i'},
		{"track-number", required_argument, NULL, 'n'},
		{"disc-template", required_argument, NULL, 'd'},
		{"track-template", required_argument, NULL, 't'},
		{"version", no_argument, NULL, 'V'},
		{NULL, 0, NULL, 0}
	};

	progname = argv[0];

	while (-1 != (c = getopt_long(argc, argv, "hi:n:d:t:V", longopts, NULL))) {
		switch (c) {
		case 'h':
			usage(0);
			break;
		case 'i':
			if (0 == strcmp("cue", optarg)) {
				format = CUE;
			} else if (0 == strcmp("toc", optarg)) {
				format = TOC;
			} else {
				fprintf(stderr, "%s: error: unknown input file"
				        " format `%s'\n", progname, optarg);
				usage(1);
			}
			break;
		case 'n':
			trackno = atoi(optarg);
			break;
		case 'd':
			d_template = optarg;
			break;
		case 't':
			t_template = optarg;
			break;
		case 'V':
			version();
			break;
		default:
			usage(1);
			break;
		}
	}

	/* If no disc or track template is set, use the defaults for both. */
	/* TODO: alternative to strdup to get variable strings? */
	if (NULL == d_template && NULL == t_template) {
		d_template = strdup(D_TEMPLATE);
		t_template = strdup(T_TEMPLATE);
	} else {
		if (NULL == d_template) {
			d_template = strdup("");
		}

		if (NULL == t_template) {
			t_template = strdup("");
		}
	}

	/* Translate escape sequences. */
	translate_escapes(d_template);
	translate_escapes(t_template);

	/* What we do depends on the number of operands. */
	if (optind == argc) {
		/* No operands: report information about stdin. */
		ret = info("-", format, trackno, d_template, t_template);
	} else {
		/* Report information about each operand. */
		for (; optind < argc; optind++) {
			ret = info(argv[optind], format, trackno, d_template, t_template);
			/* Exit if info() returns nonzero. */
			if (!ret) {
				break;
			}
		}
	}

	return ret;
}
