#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/statvfs.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <signal.h>
#include <glib.h>
#include <gpod-1.0/gpod/itdb.h>
//#include <id3tag.h>

typedef struct {
	char path[PATH_MAX];
	off_t size;
} musicfile;

/**
 * Initialize random generator.
 */
void initRandom() {
	int init;
	time_t t;
	init = time(&t);
	srandom(init);
}

/*
 * Check that a string ends with a specific value (case careless).
 * @param const char *str : The string to check.
 * @param const char *patternd : The desired end.
 * @return True if str ends with pattern.
 */
int endsWith(const char *str, const char *pattern) {
	int length = strlen(str);
	char* end = (char*) (str + length - strlen(pattern));
	if (strcasecmp(end, pattern) == 0) {
		return 1;
	}
	else {
		return 0;
	}
}

/*
 * Browse the specified directory and return a GList * containg all MP3 files found.
 * @param const char *path : The path to the directory containing the music files.
 * @param GList *musiclist : The list where the foud files will be stored. 
 * @param const int verbose : A boolean, true for verbose output.
 * @return GList * containing all music files found.
 */
GList *findMusic(const char *path, GList *musiclist, const int verbose) {
	DIR *dir;
	struct dirent *file;
	if (dir = opendir(path)) {
		while ((file = readdir(dir)) != NULL) {
			char filepath[PATH_MAX];
			sprintf(filepath, "%s/%s", path, file->d_name);
			struct stat st;
			if (strcmp(file->d_name, ".") != 0 && strcmp(file->d_name, "..") != 0) {
				if (stat(filepath, &st) == -1) { perror("stat"); }
				else {
					if ((st.st_mode & S_IFMT) == S_IFREG && endsWith(file->d_name, ".mp3")) {
						musicfile *m = malloc(sizeof(musicfile));
						strcpy(m->path, filepath);
						m->size = st.st_size;
						musiclist = g_list_prepend(musiclist, m);
						if (verbose) { printf("found: %s\n", m->path); }
					}
					else {
						musiclist = findMusic(filepath, musiclist, verbose);
					}
				}
			}
		}
		closedir(dir);
	}
	return musiclist;
}

/*
 * Calculates the available space on a filesystem. 
 * @param const char *mountpoint : A path to a directory located on the filesystem you want the available space.
 * @return off_t containing the amount of available octets.
 */
off_t getAvailableSpace(const char *mountpoint) {
	struct statvfs st;
	if (statvfs(mountpoint, &st) == -1) { perror("stat"); }
	off_t available_space = st.f_bavail * st.f_bsize;
	return available_space;
}

/*
 * Makes a list with random files until amount octets.
 * @param const off_t amout : The amout of octets to put on the iPod.
 * @param GList *musiclist : The list of available music file. This list is modified by this function : each file which is added to the iPod is removed from the list.
 * @param const int verbose : A boolean, true for verbose output.
 * @return GList * containing the files to copy.
 */
GList * getRandomFilesToIpodUntil(const off_t amount, GList *musiclist, const int verbose) {
	initRandom();
	int tries_count = 0;
	off_t total = 0;
	int length = g_list_length(musiclist);
	GList *randomfiles = NULL;
	while ( total < amount && length > 0) {
		int r = (random() % g_list_length(musiclist));
		GList *element = g_list_nth(musiclist, r);
		musicfile *m = (musicfile*) element->data;
		if (m->size < amount - total) {
		// the selected music file length is lower than the remaining space
		// we add it
			tries_count = 0;
			randomfiles = g_list_prepend(randomfiles, element->data);
			musiclist = g_list_remove_link(musiclist, element);
			total = total + ((musicfile*) element->data)->size;
			length = g_list_length(musiclist);
			if (verbose) { printf("add: %s\n", m->path); }
		}
		else if (tries_count < 5) {
		// the selected music file length is greater than the remaining free space
		// and we haven't tried moran than 5 times.
		// we increment the tries counter
			tries_count++;
		}
		else {
		// 5 tries happened, so we leave
			total = amount;
		}
	}
	return randomfiles;
}

/*
 * Copies the specified file to the ipod's database.
 * @param const musicfile *file : The file description to copy.
 * @param Itdb_iTunesDB *itdb : The iPod's db.
 * TODO: add id3tags
 */
void ipod_putFile(const musicfile *file, Itdb_iTunesDB *itdb) {
	GError *error = NULL; 
	Itdb_Track *t = itdb_track_new();
	// TODO convert titles to utf8
	//t->title = g_path_get_basename(file->path);
	itdb_track_add(itdb, t, -1);
	itdb_cp_track_to_ipod(t, file->path, &error);
	if (error) {
		if (error->message) {
			printf("%s\n", error->message); 
		}
		g_error_free(error);
		error = NULL;
	}
}

/*
 * Launch a thread that monitor a file system given with param 'path', and shows a progressbar until its available space lowed of 'size' octets.
 * @param const char *path : A path to a directory located on the filesystem to monitor.
 * @param const off_t *size : The amount of octets the progressbar represents.
 */
void showProgressBar(const char *path, const off_t size) {
	off_t start = getAvailableSpace(path);
	off_t end = start - size;
	pid_t pid = fork();
	if (pid == 0) { //fils
	int stop = 0;
	while (!stop) {
		printf("\r|");
		off_t i; 
		for (i = start; i > end; i = i - size/50) {
			char toPrint;
			if (getAvailableSpace(path) < i) { 
				toPrint = '='; 
			}
			else { toPrint = ' '; }
			printf("%c", toPrint);
		}
		int percent = (start-getAvailableSpace(path))*100/size;
		if (percent > 100 ) percent = 100;
		printf("| %d%%", percent);
		fflush(stdout);
		if (getAvailableSpace(path) < end) stop = 1;
		usleep(1500);
	}
	printf("\n");
	exit(0);
	}
}

/*
 * Method called when a SIGUSR1 is received.
 */
void stopWaiter() {
	printf("\b");
	exit(0);
}

/*
 * Launch a thread that shows a little bar going round and round to make the user waiting.
 */
pid_t showWaiter() {
	pid_t pid = fork();
	if (pid == 0 ) { // fils
		if (signal(SIGUSR1, stopWaiter) == SIG_ERR) { exit(0); }
		int stop = 0;
		int i = 0;
		char c[4] = {'/', '-', '\\', '|'};
		printf(" ");
		for (i = 0; ; i= (i+1)%4) {
			sleep(1);
			printf("\b%c", c[i]);
			fflush(stdout);
		}
	}
	return pid;
}

/*
 * Free a music file struct.
 */
void musicfile_free(musicfile *m) {
	free(m);
}

/*
 * Returns the index of a parameter contained in argv[]. If it not exists, returns -1.
 * @param const int argc : The size of table argv[].
 * @param const char *argv[] : All the paramters where you want to search.
 * @param const char *param : The parameter looked for (ex: '--param').
 * @param const char *shorcut : The optional shortcut. (ex '-p') or NULL.
 */
int existArg(const int argc, const char *argv[], const char *param, const char *shortcut) {
	int i;
	for (i = 1; i < argc; i++) {
		if (strcmp(argv[i], param) == 0) {
			return i;
		}
		if (shortcut != NULL) {
			if (strcmp(argv[i], shortcut) == 0) {
				return i;
			}
		}
	}
	return -1;
}

/**
 * Checks if the parameter is a number : [1-9]
 * @param const char c : The char to check.
 * @return 1 is it is a number.
 */
int isNumeric(const char c) {
	char num[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
	int i;
	for (i = 0; i < 9; i++) {
		if (c == num[i]) {
			return 1;
		}
	}
	return 0;
}

/*
 * Checks the integrity of parameter amount : [1-9]+(M|G|K)*
 * @param const char *a : A char*.
 * @return : -1 if the param 'a' doesn't math the abose regular expression, of the length it represents in octet.
 */
int checkParamAmount(const char *a) {
	int i;
	for (i = 0; i < strlen(a)-1; i++) {
		if (!isNumeric(a[i])) {
			return -1; 
		}
	}
	char *_length = malloc((strlen(a)-1) * sizeof(char));
	strncpy(_length, a, strlen(a)-1);
	int length = atoi(_length);
	free(_length);
	if (endsWith(a, "k") || endsWith(a, "K")) {
		return length * 1024;
	}
	if (endsWith(a, "m") || endsWith(a, "M")) {
		return length * 1024 * 1024;
	}
	if (endsWith(a, "m") || endsWith(a, "M")) {
		return length * 1024 * 1024 * 1024;
	}
	if (isNumeric(a[strlen(a)-1])) {
		return atoi(a);
	}
	return -1;
}

/*
 * Convert a length in octets to a more efficient unit making reading easier.
 * @param const off_t size : A length in octets.
 * @return a char*
 */
char *toHumanReadable(const off_t size) {
	char *readable = malloc(sizeof(char) * 14);
	int ko = size/1024;
	int mo = ko/1024;
	int go = mo/1024;
	if (size < 1024) {
		sprintf(readable, "%d", size);
	}
	else if (ko < 1024) {
		sprintf(readable, "%dKo", ko);
	}
	else if (mo < 1024) {
		sprintf(readable, "%dMo", mo);
	}
	else {
		sprintf(readable, "%dGo", go);
	}
	return readable;
}

/*
 * Exit if 'error' contains an error, else do nothing.
 */
void exitIf(GError *error) {
	if (error) {
		if (error->message) {
			fprintf(stderr, "%s\n", error->message);
		}
		else {
			fprintf(stderr, "Unexpected error\n");
		}
	}
}

/**
 * Show help and exit.
 * @param const char *app_name : Usually argv[0].
 */
void usage(const char* app_name) {
        printf("Usage: %s <mountpoint> [--amount <xxS>] [--music <dir>]\n\n"
                "\t <mountpoint>\tLe chemin d'accès au point de montage de l'iPod (/media/ipod).\n"
                "\t -a, --amount <xxS>\tLa quantité de musique à copier sur l'iPod, où 'xx' prend une valeur entière et 'S' prend les valeurs 'M' ou G'.\n"
                "\t -m, --music <dir>\tLe chemin d'accès vers le repertoire contenant la musique.\n", app_name);
        exit(0);
}


// TODO add path in perrors
int main (const int argc, const char *argv[]) {
	if (argc < 2) { usage(argv[0]); }

	const char *mountpoint;
	const char *musicdir;
	off_t amount;
	int verbose;

	// checking mountpoint
	struct stat st;
	if (stat(argv[1], &st) == -1) { perror("stat"); exit(0); }
	if ((st.st_mode & S_IFMT) != S_IFDIR ) { printf("'%s' is not a directory\n", argv[1]); usage(argv[0]); }
	mountpoint = argv[1];
	printf("Using '%s' as mountpoint\n", mountpoint);

	// checking --verbose arg
	int v = existArg(argc, argv, "--verbose", "-v");
	if (v == -1) { verbose = 0; }
	else { verbose = 1; }

	// checking --amount arg
	int a = existArg(argc, argv, "--amount", "-a");
	if (a == -1) {
		amount = getAvailableSpace(mountpoint) - 10*1024*1024;
		printf("Using all available space on ipod : %s\n", toHumanReadable(amount));
	}
	else {
		amount = checkParamAmount(argv[a+1]);
		if (amount <=0) { fprintf(stderr, "'%s' is not a correct value for arg --amount\n", argv[a+1]); exit(0); }
		if (amount > getAvailableSpace(mountpoint) - 10*1024*1024) { 
			amount = getAvailableSpace(mountpoint) - 10*1024*1024; 
			printf("'%d' is higher than the usable space on the ipod, using %d instead", argv[a+1], toHumanReadable(amount)); 
		}
		printf("Will fill the iPod with %s of music files\n", toHumanReadable(amount));
	}

	// checking --music arg
	int m = existArg(argc, argv, "--music", "-m");
	if (m == -1) {
		musicdir = getenv("PWD");
		printf("Searching music in the current directory : '%s'\n", musicdir);
	}
	else {
		if (stat(argv[m+1], &st) == -1) { perror("stat"); exit(0); }
		if ((st.st_mode & S_IFMT) != S_IFDIR ) { printf("'%s' is not a directory\n", argv[m+1]); usage(argv[0]); }
		musicdir = argv[m+1];
		printf("Searching music in '%s'\n", musicdir);
	}
	
	// finding all mp3 files
	GList *allmusic = NULL;
	allmusic = findMusic(musicdir, allmusic, verbose);

	// selecting random files
	GList *files = getRandomFilesToIpodUntil(amount, allmusic, verbose);

	// copying them to the ipod
	GError *error = NULL;
	Itdb_iTunesDB *db = itdb_parse(mountpoint, &error);

	if (error) {
		if (error->message) {
			if (strncmp(error->message, "iTunes directory not found", 26)) {
				printf("%s\nTrying to create it\n", error->message);
			}
			else {
				// unexpected error, leaving
				fprintf(stderr, "%s\n", error->message); 
				exit(0);
			}
		}
		g_error_free(error);
		error = NULL;
		// iPod isn't initialized, TODO ask the user for model
		itdb_init_ipod(mountpoint, "xB225", g_path_get_basename(mountpoint), &error);
		exitIf(error);
		db = itdb_parse(mountpoint, &error);
		exitIf(error);
	}

//showProgressBar(mp, size*1024*1024);

	g_list_foreach(files, (GFunc) ipod_putFile, db);
	
	int msg;
	wait(&msg);
	printf("\nPlease wait while writing iTunes DB to the iPod "); fflush(stdout);
//pid_t waiter = showWaiter();
	itdb_write(db, &error);
	// TODO add error reporter
	// TODO this is only for Shuffle, do it only if user says he had a Shuffle
	itdb_shuffle_write(db, &error);
//kill(waiter, SIGUSR1);
//wait(&msg);
	exitIf(error);
	printf(" [done]\n");

	// freeing memory
	itdb_free(db);
	g_list_foreach(allmusic, (GFunc) musicfile_free, NULL);
	g_list_foreach(files, (GFunc) musicfile_free, NULL);
	g_list_free(allmusic); g_list_free(files);

	exit(0);
}
