/*
 * This file is part of Ample.
 *
 * Ample is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Ample is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Ample; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * $Id: entries.c,v 1.33 2002/12/08 14:10:34 alphix Exp $
 *
 * This file contains all functions related to inserting, removing,
 * debugging, searching etc. the filetree that Ample builds of all MP3
 * files it can find. Also functions for getting entries from directories,
 * M3U files etc.
 */


#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <libgen.h>
#include <ctype.h>
#if HAVE_LIMITS_H
#include <limits.h>
#endif
#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# if HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
# if HAVE_NDIR_H
#  include <ndir.h>
# endif
#endif

#include "ample.h"
#include "entries.h"
#include "client.h"
#include "helper.h"

/* The root of the virtual filetree where we store all available MP3 files */
mp3entry *root = NULL;


/*
 * Given a virtual path, finds the corresponding mp3entry.
 *
 * Arguments: base - where to start the search (only entries below or adjacent
 *                   to this one will be searched)
 *            name - the path to search for
 *
 * Returns: the mp3entry if found, else NULL
 */
mp3entry *
findentrybypath(mp3entry *base, char *name) 
{
	char *namec = strdup(name);
	char *token;
	mp3entry *tmp = base;

	token = strtok(namec, "/");

	if(token == NULL) {
		free(namec);
		return tmp;
	}
	
	while(token != NULL && tmp != NULL) {
		while(tmp && strcmp(tmp->name, token))
			tmp = tmp->sibling;
		
		token = strtok(NULL, "/");
		
		if(tmp && !token) {
			debug(3, "Find: found the last part with %s\n", 
			      tmp->name);
			break;
		} else if(tmp) {
			debug(3, "Find: found the part with %s\n", 
			      tmp->name);
			tmp = tmp->child;
		}
	}

	free(namec);
	if(tmp != NULL && tmp->child)
		return tmp->child;
	else
		return tmp;
}


/*
 * Given an mp3entry, finds the virtual path.
 * 
 * Arguments: base - where to start the search (only entries below or adjacent
 *                   to this one will be searched)
 *            tofind - the entry to look for
 *            path - the base path to add to the beginning of the path to the
 *                   file, most functions would set this argument to ""
 *
 * Returns: a path stored in malloc:ed memory or NULL if entry wasn't found
 */
char *
findpathbyentry(mp3entry *base, mp3entry *tofind, char *path)
{
	mp3entry *tmp = base;
	char *subpath;
	char *pathc;

	while(tmp != NULL && tmp != tofind) {
		if(tmp->child) {
			pathc = malloc(strlen(path) + strlen(tmp->name) + 2);
			sprintf(pathc, "%s/%s", path, tmp->name);
			subpath = findpathbyentry(tmp->child, tofind, pathc);
			free(pathc);
			if(subpath)
				return subpath;
		}
		tmp = tmp->sibling;
	}

	if(tmp == tofind) {
		subpath = malloc(strlen(path) + strlen(tmp->name) + 2);
		sprintf(subpath, "%s/%s", path, tmp->name);
		return subpath;
	} else {
		return NULL;
	}
}


/*
 * Finds the index of a given entry. Index is based on a depth-first
 * search where only files are given a number.
 *
 * Arguments: base - where to start the search (only entries below or adjacent
 *                   to this one will be searched)
 *            tofind - the entry to look for
 *            index - a pointer to the integer which will hold the result
 * 
 * Returns: TRUE if the index was found, else FALSE and value of index is
 *          undefined
 */
bool 
findindexbyentry(mp3entry *base, mp3entry *tofind, int *index)
{
	mp3entry *tmp = base;
	int count = 0;

	if(!tofind)
		return FALSE;

	while(tmp != NULL) {
		if(IS_FILE(tmp))
			(*index)++;
		if(tmp == tofind)
			return TRUE;
		tmp = tmp->sibling;
	}		
	
	tmp = base;
	
	while(tmp != NULL) {
		if(IS_DIR(tmp) && findindexbyentry(tmp->child, tofind, index))
			return TRUE;
		tmp = tmp->sibling;
	}

	return FALSE;
}


/*
 * Finds en entry given an index. Index is based on a depth-first search 
 * where only files are given a number.
 *
 * Arguments: base - where to start the search (only entries below or adjacent
 *            index - a pointer to the index for the entry
 *                    (*FIXME* the index shouldn't be modified)
 *
 * Returns: the entry if found, else NULL, index is always undefined
 */
mp3entry * 
findentrybyindex(mp3entry *base, int *index)
{
	mp3entry *tmp = base;
	mp3entry *result = NULL;

	if(*index == 0 || base == NULL)
		return NULL;

	while(tmp != NULL) {
		if(IS_FILE(tmp))
			(*index)--;
		if((*index) == 0)
			return tmp;
		else
			tmp = tmp->sibling;
	}

	tmp = base;
	
	while(tmp != NULL) {
		if(IS_DIR(tmp) && 
		   (result = findentrybyindex(tmp->child, index)))
			return result;
		tmp = tmp->sibling;
	}

	return NULL;
}


/*
 * Utility function to find the first file (not dir) in a breadth-first
 * search of the filetree.
 *
 * Arguments: base - where to start the search (only entries below or adjacent
 *                   to this one will be searched)
 *            recursive - should subdirs be searched as well?
 *            result - a pointer to an mp3entry pointer which is set to
 *                     the first file if it is found, else NULL
 *
 * Returns: void
 */
static void 
findfirstfile(mp3entry *base, bool recursive, mp3entry **result)
{
	mp3entry *tmp = base;
	
	if(tmp == NULL)
		return;

	while(tmp != NULL && *result == NULL) {
		if(IS_FILE(tmp)) {
			*result = tmp;
			return;
		}
		tmp = tmp->sibling;
	}

	if(recursive) {
		tmp = base;
		while(tmp != NULL && *result == NULL) {
			if(IS_DIR(tmp))
				findfirstfile(tmp->child, recursive, result);
			tmp = tmp->sibling;
		}
	}
}


/*
 * Utility function to find the last file (not dir) in a breadth-first
 * search of the filetree.
 *
 * Arguments: base - where to start the search (only entries below or adjacent
 *                   to this one will be searched)
 *            recursive - should subdirs be searched as well?
 *            result - a pointer to an mp3entry pointer which is set to
 *                     the last file if it is found, else NULL
 *
 * Returns: void
 */
static void 
findlastfile(mp3entry *base, bool recursive, mp3entry **result)
{
	mp3entry *tmp = base;
	
	if(tmp == NULL)
		return;

	while(tmp != NULL) {
		if(IS_FILE(tmp))
			*result = tmp;
		tmp = tmp->sibling;
	}

	if(recursive) {
		tmp = base;
		while(tmp != NULL) {
			if(IS_DIR(tmp) && recursive)
				findlastfile(tmp->child, recursive, result);
			tmp = tmp->sibling;
		}
	}
}


/*
 * Given a base, finds the index of the first and last playable files below
 * or adjacent to that file. Good for playing all the files in a subdirectory
 * for instance since you know all files min -> max are valid.
 *
 * Arguments: base - where to start the search (only entries below or adjacent
 *                   to this one will be searched)
 *            recursive - should subdirs be searched as well?
 *            min - a pointer to an integer which will be filled in with the
 *                  value of the lowest index
 *            max - a pointer to an integer which will be filled in with the
 *                  value of the highest index
 *
 * Returns: TRUE if the operation was successful, else FALSE
 */
bool 
getrange(mp3entry *base, bool recursive, int *min, int *max)
{
	mp3entry *tmp;

	*min = 0;
	*max = 0;

	tmp = NULL;
	findfirstfile(base, recursive, &tmp);
	findindexbyentry(root, tmp, min);
	tmp = NULL;
	findlastfile(base, recursive, &tmp);
	findindexbyentry(root, tmp, max);
	
	if(max == 0 || min == 0)
		return FALSE;
	else
		return TRUE;
}


/*
 * Pretty-print the virtual filetree, mostly useful for debugging purposes.
 *
 * Arguments: indent - how much to indent the printout, used for recursing
 *                     purposes, other functions should generally set this
 *                     to 0
 *            entry - where to start the printing (entries below or adjacent
 *                   to this one will be printed)
 *
 * Returns: void
 */
void 
showtree(int indent, mp3entry *entry) 
{
	char *start;
	int i;
	
	if(!entry)
		return;
	
	start = malloc(4 * indent + 1);
	*start = 0;
	for(i = 0; i < indent; i++)
		sprintf((start + 4 * i), "|   ");
	
	if(IS_DIR(entry)) {
		debug(1, "%s|---<%s>\n", start, entry->name);
		showtree(indent + 1, entry->child);
	} else {
		debug(1, "%s|-%s [%i:%02i-%d-%d-%i]\n", start, entry->name, 
		      entry->length / 60, entry->length % 60, 
		      HASID3V1(entry), HASID3V2(entry), entry->length);
	}

	free(start);
	showtree(indent, entry->sibling);
}


/*
 * Frees an entry and all dynamic memory referenced by it.
 *
 * Arguments: tofree - the entry to free
 * 
 * Returns: void
 */
static void 
freeentry(mp3entry *tofree)
{
	if(!tofree)
		return;
	if(tofree->name)
		free(tofree->name);
	if(tofree->path)
		free(tofree->path);
	if(tofree->title)
		free(tofree->title);
	free(tofree);
}


/*
 * Adds an entry to the virtual filetree. If an old entry with the same name
 * exists in the tree an error message is printed and the new entry is freed. 
 *
 * Arguments: baseptr - where to add the new entry, it is added as a sibling
 *                      to this entry
 *            toadd - the entry to add
 *
 * Returns: void
 */
void 
addentry(mp3entry **baseptr, mp3entry * toadd) 
{
	mp3entry *tmp;
	mp3entry **prevptr;
	int status;
/*
	logmsg("going to add entry:\n");
	dumpentry(toadd);
*/
	prevptr = baseptr;
	tmp = *baseptr;

	while(tmp != NULL) {
		
		if(toadd->type > tmp->type)
			status = 1;
		else
			status = strcmp(toadd->name, tmp->name);
		
		if(status < 0) {
			/*logmsg("I think it should be added to this file:\n");*/
/*			dumpentry(tmp);
			showtree(0, root);*/
			break;
		} else if(status == 0) {
/*			logmsg("addentry: two items with the same name (%s)\n",
			tmp->name);*/
			freeentry(toadd);
			return;
		} else {
			prevptr = &(tmp->sibling);
			tmp = tmp->sibling;
		}
	}
	
	*prevptr = toadd;
	toadd->sibling = tmp;
}


/*
 * Removes and frees an entry from the virtual filetree.
 *
 * Arguments: baseptr - where to start looking for the entry
 *            toremove - the entry to remove
 *
 * Returns: TRUE if an entry was removed, else FALSE
 */
bool 
removeentry(mp3entry **baseptr, mp3entry *toremove) 
{
	bool result = FALSE;
	mp3entry *tmp;
	mp3entry **prevptr;
	
	prevptr = baseptr;
	tmp = *baseptr;
	
	while(tmp != NULL && tmp != toremove) {
		result = removeentry(&(tmp->child), toremove);
		prevptr = &(tmp->sibling);
		tmp = tmp->sibling;
	}

	if(!tmp)
		return result;

	while(tmp->child)
		removeentry(&(tmp->child), tmp->child);
	
	debug(3, "Removing %s\n", tmp->name);
	*prevptr = tmp->sibling;
	freeentry(tmp);
	return TRUE;
}


/*
 * Clears and frees the entire virtual tree of files.
 *
 * Arguments: rootentry - where to start
 *
 * Returns: void
 */
void 
cleartree(mp3entry **rootentry) 
{
	while(*rootentry != NULL)
		removeentry(rootentry, *rootentry);
}


/*
 * Writes out the entire tree to a specific memory location and
 * changes all pointers accordingly. No checking of the size of
 * the memory area is done so make sure it's big enough
 * (as big as spaceneeded() says).
 *
 * Arguments: basedest - the destination to write to
 *            baseentry - where to start
 *
 * Returns: the number of bytes written
 */
int 
writetree(void *basedest, mp3entry *baseentry)
{
	void *tmp = basedest;
	mp3entry *from = baseentry;
	mp3entry *to;
	int towrite;
	int written = 0;

	while(from) {
		to = tmp;
		memcpy(to, from, sizeof(mp3entry));
		written += sizeof(mp3entry);
		tmp += sizeof(mp3entry);

		if(from->name) {
			to->name = tmp;
			towrite = strlen(from->name) + 1;
			memcpy(to->name, from->name, towrite);		
			written += towrite;
			tmp += towrite;
		}

		if(from->path) {
			to->path = tmp;
			towrite = strlen(from->path) + 1;
			memcpy(to->path, from->path, towrite);
			written += towrite;
			tmp += towrite;
		}

		if(from->title) {
			to->title = tmp;
			towrite = strlen(from->title) + 1;
			memcpy(to->title, from->title, towrite);
			written += towrite;
			tmp += towrite;
		}

		if(from->child) {
			to->child = tmp;
			towrite = writetree(to->child, from->child);
			written += towrite;
			tmp += towrite;
		}

		if(from->sibling)
			to->sibling = tmp;
		from = from->sibling;
	}

	return written;
}


/*
 * Counts the amount of files in the virtual tree.
 *
 * Arguments: baseentry - where to start
 *
 * Returns: the amount of files
 */
int 
countentries(mp3entry *baseentry) 
{
	mp3entry *tmp = baseentry;
	int count = 0;

	while(tmp != NULL) {
		if(IS_FILE(tmp))
			count++;
		count += countentries(tmp->child);
		tmp = tmp->sibling;
	}

	return count;
}


/*
 * Calculates the amount of memory that the virtual filetree uses.
 *
 * Arguments: baseentry - where to start
 *
 * Returns: the amount of memory used
 */
int 
spaceneeded(mp3entry *baseentry) 
{
	mp3entry *tmp = baseentry;
	int count = 0;

	while(tmp != NULL) {
		count += sizeof(mp3entry);
		if(tmp->name)
			count += strlen(tmp->name) + 1;
		if(tmp->path)
			count += strlen(tmp->path) + 1;
		if(tmp->title)
			count += strlen(tmp->title) + 1;
		count += spaceneeded(tmp->child);
		tmp = tmp->sibling;
	}

	return count;
}


/*
 * Prints all information about a specific entry, good for debugging.
 *
 * Arguments: entry - the entry to analyze
 *
 * Returns: void
 */
void 
dumpentry(mp3entry *entry)
{
	if(entry == NULL) {
		debug(1,"<dump of node at NULL>\n");
		return;
	}

	debug(1,"<dump of node at %p>\n", entry);
	debug(1, "|->type: %i\n", entry->type);
	if(entry->path)
		debug(1,"|->path: %p - %s\n", entry->path, entry->path);
	else
		debug(1,"|->path: NULL\n");
	if(entry->name)
		debug(1,"|->name: %p - %s\n", entry->name, entry->name);
	else
		debug(1,"|->name: NULL\n");		
	debug(1,"|->filesize: %i\n", entry->filesize);
	debug(1,"|->id3v1len: %i\n", entry->id3v1len);
	debug(1,"|->id3v2len: %i\n", entry->id3v2len);
	if(entry->title)
		debug(1,"|->title: %p - %s\n", entry->title, entry->title);
	else
		debug(1,"|->title: NULL\n");
	if(entry->sibling)
		debug(1,"|->sibling: %p\n", entry->sibling);
	else
		debug(1,"|->sibling: NULL\n");
	if(entry->child)
		debug(1,"|->child: %p\n", entry->child);
	else
		debug(1,"|->child: NULL\n");
}


/*
 * Checks which type of file/dir path points to. Can be dir/mp3/m3u, also does 
 * some sanity checks on these files so if this function says they're ok they 
 * should be. buf is filled in if the path is valid. Is there no end 
 * to the advances of modern technology!? :-)
 *
 * Arguments: path - the path to examine, can be both relative and absolute
 *            buf - filled in with info about the path
 *
 * Returns: the type of the path (as defined in the header file)
 */
static int 
checkpathtype(char *path, mp3entry **buf)
{
	struct stat statbuf;
	char *dirc, *basec, *bname, *dname;
	char *owd, *cwd;
	int retval = TYPE_INVALID;

	if(stat(path, &statbuf)) {
		/* 
		 * This *should* protect against symlink loops since that 
		 * would give ELOOP 
		 */
		logmsg("Error, unable to stat path - %s (reason: %s)\n", path, strerror(errno));
		*buf = NULL;
		return TYPE_INVALID;
	}

	dirc = strdup(path);
	basec = strdup(path);
	dname = dirname(dirc);
	bname = basename(basec);

	if(bname[0] == '.') {
		/* File/dir shouldn't begin with . */
		debug(2, "Ignoring %s, starts with .\n", path);
	} else if(S_ISDIR(statbuf.st_mode)) {
		/* Is it a dir? */
		debug(2, "Valid dir %s\n", path);

		*buf = (mp3entry *)malloc(sizeof(mp3entry));
		memset(*buf,0,sizeof(mp3entry));

		(*buf)->name = strdup(bname);
		(*buf)->type = TYPE_DIR;

		retval = TYPE_DIR;
	} else if(!S_ISREG(statbuf.st_mode)) {
		/* If not a dir, it must be a regular file */
		debug(2, "Ignoring %s, not a dir and not a regular file\n", path);
	} else if(strlen(bname) < 5) {
		/* Filename must be at least as long as x.mp3 or x.m3u */
		debug(2, "Ignoring %s, too short filename\n", path);
	} else if(statbuf.st_size == 0) {
		/* File size must be > 0 */
		debug(2, "Ignoring %s, zero size\n", path);
	} else if(!strcasecmp((bname + strlen(bname) - 4),".mp3")) {
		/* Does the filename end with .mp3 (case insensitive)? */
		debug(2, "Valid mp3 file %s\n", path);

		*buf = (mp3entry *)malloc(sizeof(mp3entry));
		memset(*buf,0,sizeof(mp3entry));
	
		if(!strcmp(dname, ".")) {
			cwd = mgetcwd();
			(*buf)->path = (char *)malloc(strlen(cwd) + 
						      strlen(bname) + 2);
			sprintf((*buf)->path, "%s/%s", cwd, bname);
			free(cwd);
		} else {
			owd = mgetcwd();
			chdir(dname);
			cwd = mgetcwd();
			
			(*buf)->path = (char *)malloc(strlen(cwd) + 
						      strlen(bname) + 2);
			sprintf((*buf)->path, "%s/%s", cwd, bname);
			
			chdir(owd);
			free(owd);
			free(cwd);
		}

		(*buf)->name = strdup(bname);
		(*buf)->type = TYPE_MP3;
		(*buf)->filesize = statbuf.st_size;
		
		checkmp3info((*buf));
		retval = TYPE_MP3;

	} else if(!strcasecmp((bname + strlen(bname) - 4),".m3u")) {
		/* Does the filename end with .m3u (case insensitive)? */
		debug(2, "Valid m3u file %s\n", path);

		*buf = (mp3entry *)malloc(sizeof(mp3entry));
		memset(*buf,0,sizeof(mp3entry));

		(*buf)->name = strdup(bname);
		(*buf)->type = TYPE_DIR;
		
		retval = TYPE_M3U;
	} else {
		/* Odd filename */
		debug(2, "Ignoring %s, weird filename\n", path);
	}

	if(retval == TYPE_INVALID)
		*buf = NULL;

	free(dirc);
	free(basec);
	return retval;
}


/*
 * Reads the contents of a directory, adds all valid MP3 files to virtual
 * filetree and (depending on configuration options) recursively scans
 * all subdirs.
 *
 * Arguments: path - the basepath in the virtual file tree
 *            baseptr - where to add files in the virtual file tree
 *
 * Returns: void
 */
static void 
parsedir(char *path, mp3entry **baseptr)
{
	DIR *dir;
	struct dirent *dirent;
	char *cwd,*owd;
	char *dname,*bname;
	mp3entry *filebuf;

	owd = mgetcwd();
	if(chdir(path)) {
		logmsg("Incorrect path for mp3 files - %s\n", path);
		free(owd);
		return;
	} else {
		cwd = mgetcwd();
	}

	if (!(dir = opendir(".")))
		die("opendir()\n");
	
	while((dirent = readdir(dir))) {
		
		switch(checkpathtype(dirent->d_name, &filebuf)) {
		case TYPE_MP3:
			addentry(baseptr, filebuf);
			break;
		case TYPE_DIR:
			if(gconf.recursive) {
				addentry(baseptr, filebuf);
				parsedir(dirent->d_name, &(filebuf->child));
			} else {
				freeentry(filebuf);
			}
			break;
		case TYPE_M3U:
			freeentry(filebuf);
			/* fall trough */
		default:
			/* This includes TYPE_INVALID */
			continue;
		}
		
		errno = 0;
	}
	
	if (errno)
		die("readdir()\n");
	
	chdir(owd);
	free(owd);
	free(cwd);
	closedir(dir);
}


/*
 * Parses an M3U file and tries to add all files listed in it to the
 * virtual file tree.
 *
 * Arguments: baseptr - where to add the files
 *            path - path to the M3U file
 *
 * Returns: void
 */
static void 
parsem3u(mp3entry **baseptr, char *path) 
{
	FILE *m3u;
	mp3entry *mp3buf;
	char line[1000];
	char *start,*end;
	char *tmp;
	char *dirc, *basec, *bname, *dname;
	char *owd;

	if(!(m3u = fopen(path, "r"))) {
		logmsg("Unable to open .m3u file %s\n", path);
		return;
	}

	dirc = strdup(path);
	basec = strdup(path);
	dname = dirname(dirc);
	bname = basename(basec);
	owd = mgetcwd();
	chdir(dname);
	
	while((fgets(line, 1000, m3u)) != NULL) {
		for(start = line; start != NULL && isspace(*start); start++);

		if(!start || *start == '#' || 
		   *start == '\0' || strlen(start) < 1)
			continue;
		
		for(end = start + strlen(start); *end == '\0' || 
			    isspace(*end) || *end == '\n'; end--);
		end++;
		*end = '\0';

		tmp = malloc(strlen(start) + 1);
		snprintf(tmp, strlen(start) + 1, "%s", start);
		
		switch(checkpathtype(tmp, &mp3buf)) {
		case TYPE_MP3:
			addentry(baseptr, mp3buf);
			break;
		case TYPE_INVALID:
			continue;
		default:
			/* Includes TYPE_M3U and TYPE_DIR */
			freeentry(mp3buf);
			break;
		}
		
		free(tmp);
	}

	chdir(owd);
	free(owd);
	free(dirc);
	free(basec);
	fclose(m3u);
}


/*
 * Iterates trough the array of paths to M3U/MP3 files and directories
 * and takes appropriate actions on each entry.
 *
 * Arguments: patharray - the array of paths to file/dirs to add
 *
 * Returns: void
 */
void 
indexpaths(char **patharray) 
{
	int i;
	mp3entry *mp3buf;

	/* Prepare the virtual file tree of MP3's */
	if(!patharray)
		return;
	
	for(i = 0; patharray[i] != NULL; i++) {
		debug(1, "Checking path for mp3 files - %s\n", patharray[i]);

		switch(checkpathtype(patharray[i], &mp3buf)) {
		case TYPE_INVALID:
			continue;
		case TYPE_MP3:
			addentry(&root, mp3buf);
			break;
		case TYPE_M3U:
			freeentry(mp3buf);
			parsem3u(&root, patharray[i]);
			break;
		case TYPE_DIR:
			freeentry(mp3buf);
			parsedir(patharray[i], &root);
			break;
		default:
			logmsg("Indexpaths: we should never end up here\n");
			break;
		}
	}
}
