/*
 *  playlist.c
 *  mod_musicindex
 *
 *  $Id: playlist.c 1022 2012-10-15 21:33:12Z varenet $
 *
 *  Created by Thibaut VARENE on Thu Mar 20 2003.
 *  Copyright (c) 2003-2004 Regis BOUDIN
 *  Copyright (c) 2003-2009,2012 Thibaut VARENE
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License version 2.1,
 *  as published by the Free Software Foundation.
 *
 */

/**
 * @file
 * Playlist management system.
 *
 * @author Regis Boudin
 * @author Thibaut Varene
 * @version $Revision: 1022 $
 * @date 2003-2009
 * @date 2012
 *
 * This is where the magic takes place. Here are the functions in charge of
 * analyzing the content of the folders requested by the client, and of
 * generating the structures and lists that will be used by the HTML
 * subsystem to generate the pretty output.
 *
 * @todo document the code path as best as possible (especiallly make_music_entry)
 */

#include "playlist.h"
#include "sort.h"

#include "playlist-flac.h"
#include "playlist-vorbis.h"
#include "playlist-mp3.h"
#include "playlist-mp4.h"

#include <stdio.h>	/* fops */
#ifdef HAVE_UNISTD_H
#include <unistd.h>	/* access */
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_DIRENT_H
#include <dirent.h>	/* opendir */
#endif

/**
 * Array of common strings widely used.
 *
 * @warning Must be kept in sync with the enum in playlist.h
 *
 * http://filext.com/detaillist.php?extdetail=M3U
 * http://filext.com/detaillist.php?extdetail=MP3
 * http://filext.com/detaillist.php?extdetail=OGG
 * http://filext.com/detaillist.php?extdetail=FLAC
 * http://filext.com/detaillist.php?extdetail=MP4
 */
const struct ftype const filetype[] = {
	{ "MP3", "audio/x-mp3" },
	{ "Vorbis", "audio/x-ogg" },
	{ "FLAC", "audio/flac" },
	{ "MP4/AAC", "audio/mp4" }
};


/**
 * Fallback function if the file could not be recognised.
 *
 * @param r Apache request_rec struct
 * @param pool Pool
 * @param in file to parse (closed on normal exit)
 * @param filename current filename
 *
 * @return head, whatever happens.
 */
static inline mu_ent *make_no_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
	const char *const filename)
{
	fclose(in);
	return NULL;
}

/**
 * Wrapper function to call the actual cache backend.
 *
 * @param r Apache request_rec struct
 * @param pool Apache memory pool
 * @param in file to parse (closed on normal exit)
 * @param filename current filename
 *
 * @return the cache data on success, head otherwise
 */
static mu_ent *make_cache_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
	const char *const filename)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);

	if (conf->cache && likely(conf->cache->make_entry)) {
		mu_ent *result = conf->cache->make_entry(r, pool, in, filename);
		if (result) {
			result->flags |= EF_INCACHE;
			return result;
		}
	}
	
	return NULL;
}

/**
 * Function pointers array.
 *
 * Of course, as we defined function pointers, we will use them in a nice
 * array. They will be called in a sequential manner. @n
 * make_cache_entry() @b MUST be called first. @n
 * @e Note : a make_dir_entry() function to handle directories has
 * already been tried, unfortunately, it implies some bad side effects.
 * Plus, it probably does not make things any quicker and increases the
 * number of function calls. (that's a personnal NB to the developpers,
 * to avoid repeating twice the same havoc as time goes by ;o)
 *
 * Functions are ordered in what we believe is a top-down order of most frequent
 * filetypes.
 */
static const make_entry_ptr const make_entry[] = {
	make_cache_entry,
#ifdef	ENABLE_CODEC_MP3
	make_mp3_entry,
#endif
#ifdef	ENABLE_CODEC_MP4
	make_mp4_entry,
#endif
#ifdef	ENABLE_CODEC_FLAC
	make_flac_entry,
#endif
#ifdef	ENABLE_CODEC_VORBIS
	make_ogg_entry,
#endif
	make_no_entry,	/* MUST be last before NULL */
	NULL
};

/** Wrapper struct for the cache subsystem */
typedef struct mu_dir {
	void *dir;
	unsigned char handled;	/**< dir is handled by the cache */
} mu_dir;

/**
 * Custom directory opener.
 * 
 * This is a first shot at the custom opendir function.
 * At the end, This wraps around the various caches for directory
 * management. Depending on what the cache back-end can do, it may add things
 * to the entries list, and generate an internal list of files left to deal
 * with. These files names will be retrieved with musicindex_readdir().
 * We mimic the opendir()/readdir()/closedir() scheme.
 *
 * @param r the Apache request
 * @param dir Abstract pointer to our custom directory handler
 * @param pack pack pointer
 * @param names structure containing the name of the directory
 * @param soptions the current extended options
 */
static inline void 
musicindex_opendir(request_rec *r, mu_dir *dir, mu_pack *const pack,
			const mu_ent_names * const names, unsigned long soptions)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);

	/* It's all we need to initialise. Don't add a call to memset */
	dir->dir = NULL;

	/* A cache is configured, try to read directly data from it */
	if (conf->cache && conf->cache->opendir)
		dir->dir = conf->cache->opendir(r, pack, names->filename, names->uri, soptions);
	
	/* Cache data could not be used, fall back to the old opendir way */
	if (dir->dir == NULL) {
		dir->dir = opendir(names->filename);
		dir->handled = 0;
	}
	else {
		dir->handled = 1;
	}
}

/**
 * Get the string of the next filename to open.
 *
 * @param r Apache request_rec struct
 * @param dir Abstract pointer to our custom directory handler
 *
 * @return pointer to the string of the filename, @b without any trailing /,
 *		or NULL if there is nothing left to open.
 */
static inline const char *musicindex_readdir(request_rec *r, mu_dir *dir)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);

	if (dir->handled == 0 ) {
		struct dirent *dstruct = NULL;
	
		dstruct = readdir((DIR *)(dir->dir));

		if (NULL == dstruct)
			return NULL;
		else
			return dstruct->d_name;
	}
	else {
		/* not all cache subsys can perform whole directory read */
		if (conf->cache && conf->cache->readdir)
			return conf->cache->readdir(dir->dir);
	}
	return NULL;
}

/**
 * Close the open directory
 *
 * @param r Apache request_rec struct
 * @param dir Abstract pointer to our custom directory handler
 */
static inline void musicindex_closedir(request_rec *r, mu_dir *dir)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);

	/* A cache is configured, try to read directly data from it */
	if (dir->handled == 0)
		closedir((DIR *)(dir->dir));
	else
		if (conf->cache->closedir)
			conf->cache->closedir(dir->dir);
}

/**
 * Check lots of details that could mean we shouldn't go through the directory.
 *
 * @param r Apache current request
 * @param local_options This function can be called in subdirectories for which
 *	the configuration can differ from the current dir's one. This paramater
 *	allows the caller to specify the options to check against.
 *
 * @return TRUE if directory descend is allowed, FALSE otherwise.
 */
static inline short
go_through_directory(request_rec *r, const unsigned short local_options)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);

	/* before dealing with a directory, sanity checks */
	/* First, is the module enabled ? */
	if (!(local_options & MI_ACTIVE))
		return FALSE;

	/* playall... is stream allowed ? */
	if (((conf->options & MI_STREAMALL) == MI_STREAMALL)
		&& !(local_options & MI_ALLOWSTREAM))
		return FALSE;

	/* Searching... Is searching allowed ? */
	if ((conf->search) && !(local_options & MI_ALLOWSEARCH))
		return FALSE;

	/* Everything is fine, go on */
	return TRUE;
}

/**
 * Add a file (and the content of the directory if requested) to the chain.
 *
 * This function creates a new entry from a file.
 * If the file is a directory and the recursive option is set, all its content
 * is also added.
 * Otherwise, if the file is "recognized", it is simply added.
 *
 * @warning If the filename is too long, it will be ignored.
 * @todo Find a way to not delegate file closing to subsystems (essentially hack vorbis)
 * @bug I'm afraid if we're asked to recurse into too many subdirs, we might
 *	end up with too many open fdesc.
 *
 * @note It'd be very nice to be able to make this function tail recursive,
 * unfortunately it looks like it's going to be rather troublesome to do so.
 * @note This function will NOT and should NEVER muck with mu_pack.fhead. This
 * should be taken care of by whoever calls it.
 *
 * @note @a r->filename is the path to the file on the filesystem,
 * while @a r->parsed_uri.path is the path to the file on the webserver,
 * so no assertion can be made on their respective length
 *
 * @param pool Pool
 * @param r Apache request_rec struct to handle log writings (debugging)
 * @param pack A pack of music entries to populate
 * @param names Names
 * @param soptions bitwise special options: these are made of the extended options flag set
 *	and are used to fiddle with the algorithms (regis please docu...)
 *
 * @return When possible, struct mu_ent correctly set up
 */
void make_music_entry(request_rec *r, apr_pool_t *pool, mu_pack *const pack,
	mu_ent_names *names, unsigned long soptions)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	mu_ent		*p = NULL;
	register unsigned short	i;
	char		*uri;
	
	/* place us at the end of the string */
	uri = r->parsed_uri.path + strlen(r->parsed_uri.path);
	
	while (*(--uri) != '/')
		continue;

	if (*(++uri) == '.')	/* we don't want "invisible" files to show */
		return;
	
	if (access(r->filename, R_OK) != 0)
		return;
	
	if (!names) {
		if (unlikely((strlen(r->filename) >= MAX_STRING) || (strlen(r->parsed_uri.path) >= MAX_STRING))) {
			mi_rerror("DBG: MAX_STRING overflow on %s and/or %s - file ignored",
				      r->filename, r->parsed_uri.path);
			return;
		}
		names = apr_palloc(pool, sizeof(mu_ent_names));
		strcpy(names->filename, r->filename);
		strcpy(names->uri, r->parsed_uri.path);
		/* remove trailing '/' if any */
		if (names->filename[strlen(names->filename)-1] == '/')
			names->filename[strlen(names->filename)-1] = '\0';
	}
		
	if ((ap_is_directory(pool, names->filename))) {
		request_rec *subreq;
		int local_options = 0;

		uri = names->uri + strlen(names->uri) - 1;
		if (*uri++ != '/')
			*uri++ = '/';
		*uri = '\0';

		if (conf->dir_per_line > 0) {
			/* pretty listing of directories with action items */
			subreq = ap_sub_req_lookup_uri(names->uri, r, NULL);

			/* soptions contains the value passed to make_music_entry
			   local_options contains the configuration for the subdirectory we're looking at
			   conf->options contains the configuration for the directory that received the request, possibly modified by treat_{post,get}_args() etc */
			soptions &= ~(MI_ALLOWFETCH|MI_ALLOWRSS);

			if (subreq != NULL) {
				local_options = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->options;
				int local_rss = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->rss_items;
				ap_destroy_sub_req(subreq);

				soptions |= (local_options & MI_ALLOWFETCH);

				if (local_rss > 0)
					soptions |= MI_ALLOWRSS;
			}
		}

		if (soptions & MI_RECURSIVE) {
			/* We were asked to recurse, let's dig in */
			mu_dir		dir_handler;
			const char * restrict filename;
			unsigned short	fn_max = MAX_STRING;
			unsigned short	uri_max = MAX_STRING;
			char * restrict fn = names->filename + strlen(names->filename);
			
			if (conf->dir_per_line < 0) {
				subreq = ap_sub_req_lookup_uri(names->uri, r, NULL);

				soptions &= ~(MI_ALLOWFETCH|MI_ALLOWRSS);

				if (subreq != NULL) {
					local_options = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->options;
					ap_destroy_sub_req(subreq);

					soptions |= (local_options & MI_ALLOWFETCH);
				}
			}

			/* We remove the recursive bit at this point. It will be reinstated
			   if it's present in conf->options, e.g. when searching */
			soptions &= ((~(MI_RECURSIVE)) | (conf->options));

			if (go_through_directory(r, local_options) == FALSE)
				return;

			/* Try opening the directory */
			musicindex_opendir(r, &dir_handler, pack, names, soptions);
			if (NULL == dir_handler.dir)
				return;

			/* up to this point, names->filename doesn't have trailing '/' */
			
			/* make sure we insert a trailing slash at this point */
			*fn++ = '/';
			*fn = '\0';
			
			/* DEV NOTE: keep in mind that uri and fn both points to
			the end of their "names->*" counterpart */
			fn_max -= strlen(names->filename);
			uri_max -= strlen(names->uri);

			/* if a cache backend can provide full listings (during opendir()) it will return NULL
			 * here. Otherwise, it will provide filenames "a la readdir()" for each file that's
			 * missing in cache */
			while ((filename = musicindex_readdir(r, &dir_handler))) {
				/* Before doing some strcpy, check we want the file and there is enough space... */
				if ((filename[0] == '.') || (strlen(filename) >= fn_max) || (strlen(filename) >= uri_max))
					continue;
				strcpy(fn, filename);
				strcpy(uri, filename);
				make_music_entry(r, pool, pack, names, soptions);
			}
			musicindex_closedir(r, &dir_handler);
			return;
		}
		else if (!(conf->options & MI_STREAM) && !(conf->options & MI_RSS) && !(conf->options & MI_TARBALL)) {
			if (!(p = NEW_ENT(pool)))
				return;
			p->filetype = -1;
		}
	}
	else { /* The file was not a directory */
		FILE *in = fopen(names->filename, "r");

		if (in == NULL)
			return;

		/* This is the end of adventures for the array of function pointers.
		   We call them sequentially, until a function has recognised
		   it (p!=head), or until we reach the end of the array. In either
		   case, the file will be closed (this is the magic loop) */
		for (i=0; (!p) && make_entry[i]; i++)
			p = make_entry[i](r, pool, in, names->filename);

	}

	/* The file was not recognised, don't go any further */
	if (!p)
		return;

	/* OK, now we have an entry, fill in the missing bits */

	p->next = pack->head; /* Add it to the linked list */

	p->uri = apr_pstrdup(pool, names->uri); /* The absolute URI will be useful at some point */
	p->filename = p->file = p->uri; /* Add the relative filename */

	/* We remove the path prefix if any. We have to treat files differently
	 * than directories because we can access a given file from various
	 * parsed_uri.path whereas directories will always be shown from their
	 * parent directory (XXX assert?) */
	if ((p->filetype >= 0) && strrchr(p->filename, '/'))
		p->filename = (strrchr(p->filename, '/') + 1);
	else if ((p->filetype < 0))
		p->filename += strlen(r->parsed_uri.path);

	/* convert options into flags - We overwrite whatever was in cache,
	   as it is an abuse to use these EF_ALLOW flags anyway: nothing like that
	   will be cached, as the setting may change while the cache info may still
	   be valid. */
	P_FLAGS_OPTIONS(p, soptions);
	if ((soptions & MI_CUSTOM) == 0)
		p->file += strlen(r->parsed_uri.path);  /* offset the path before the filename relative to request current dir, except for custom playlists */
	if (conf->options & MI_TARBALL)	/* if the user asked for a tarball, we're going to need the absolute path to access the file */
		p->filename = apr_pstrdup(pool, names->filename);

	/* In the case of directories, we want to have a pretty printed version
	 * of the directory name, for display and sorting reasons (trailing '/'
	 * in the real uri would screw alphabetical sorting). Doing the work here
	 * also saves additional work in html.c. Also on can now assert that
	 * p->title always exist (sort.c) */
	if (!(p->title)) {
#ifdef NO_TITLE_STRIP
		p->title = p->filename;
#else
		/* Copy the name removing file extension (if any) and changing '_' to ' ' */
		char *const restrict stitle = apr_pstrndup(pool, p->filename,
					((p->filetype >= 0) && strrchr(p->filename, '.')) ?	/* only strip ext if !dir */
					(strrchr(p->filename, '.') - p->filename) : /* <=> strlen(p->file) - strlen(strrchr(p->file, '.')) */
					strlen(p->filename));
		
		for (i=0; stitle[i]; i++)
			if (stitle[i] == '_')
				stitle[i] = ' ';

		/* Remove trailing '/' (directories) */
		if (stitle[--i] == '/')
			stitle[i] = '\0';

		p->title = stitle; 
#endif	/* NO_TITLE_STRIP */
	}
	
	/* We put that here for cache backends which need to "see" directories;
	 * and it's faster to retrieve p->title than to process it again. */
	if (conf->cache && (!(p->flags & EF_INCACHE)) && likely((!(conf->options & MI_QUICKPL)) && conf->cache->write))
		conf->cache->write(r, p, names->filename);
	
	/* Entry is a directory, don't go further */
	if (p->filetype < 0) {
		pack->dirnb++;
		pack->head = p;
		return;
	}

	/* Here we go if we have an ongoing search request, and the file we're
	 * looking at wasn't found in the cache. We then fall back to a basic strcasestr()
	 * check, returning only the entries matching the search criteria */
	if (unlikely((conf->search) && ((soptions & MI_CUSTOM) == 0))) {
		const char *const file = names->uri + strlen(r->parsed_uri.path);
		if ((ap_strcasestr(file, conf->search))				||
			(p->artist && (ap_strcasestr(p->artist, conf->search)))	||
			(p->album && (ap_strcasestr(p->album, conf->search)))	||
			(p->title && (ap_strcasestr(p->title, conf->search)))	);
				/* do nothing if there's a match */
		else	
			return;	/* skip if there isn't */
	}

	/* sum the total size and number of files in the pack */
	pack->fsize += p->size;
	pack->filenb++;
	pack->head = p;
	return;
}

/**
 * Creates a link list based on the custom list.
 *
 * This function generates a list containing all the files referenced in the
 * @a conf->custom_list string. It handles parameters in a way so that it can
 * handle indifferently strings for cookies or on-the-fly playlists.
 *
 * @warning This function uses a subrequest for each file. This has a significant
 *	performance impact, but OTOH this function isn't really in the 'performance
 *	path' of the module anyway.
 *
 * @param r Apache request_rec struct
 * @param pack A pack to which the generated list will be hooked
 *
 * @return a list of mu_ent on success, NULL otherwise.
 */
void build_custom_list(request_rec *r, mu_pack *const pack)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	request_rec	*subreq;
	mu_ent_names	names;
	const char	*args;
	mu_ent		*mobile_ent = NULL, *custom = NULL, *result = NULL;
	char		*p, *decodeduri = NULL;
	short		direct = 0;

	if (conf->custom_list == NULL)
		return;

	args = conf->custom_list;

	
	if (strncmp(args, "playlist=", 9) == 0)
		args += 9;	/* cookie */
	else if (strncmp(args, "file=", 5) == 0)
		direct = 1;	/* direct request (Play Selected) */

	while ((*args != '\0') && (*args != ';')) {
		int local_options;
		p = ap_getword(r->pool, &args, '&');
		if (direct && (!strncmp(p, "file=", 5))) {
			p+=5;
			ap_unescape_url(p);
		}
		else if (direct)
			continue;

		decodeduri = (char *)realloc(decodeduri, 1 + apr_base64_decode_len(p));
		if (!decodeduri)
			return;
		
		apr_base64_decode(decodeduri, p);
		/* decodeduri is unescaped. We need to escape it before
		   passing a new request to the server */
		p = ap_escape_uri(r->pool, decodeduri);
		
		/* we do that to avoid nasty accesses if somebody crafts a uri */
		subreq = ap_sub_req_lookup_uri(p, r, NULL);

		if (subreq == NULL)
			continue;

		strcpy(names.uri, subreq->parsed_uri.path);
		strcpy(names.filename, subreq->filename);

		local_options = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->options;

		ap_destroy_sub_req(subreq);
		
		/* adds entries bottom-up */
		make_music_entry(r, r->pool, pack, &names,
			(MI_CUSTOM | (local_options & MI_ALLOWFETCH)));
	}
	free(decodeduri);

	/* reorder the list top-down. Deconstify because we do something nasty */
	custom = (mu_ent *)pack->head;
	for (result = NULL; custom; custom = mobile_ent) {
		mobile_ent = (mu_ent *)custom->next;
		custom->next = result;
		result = custom;
	}
	pack->fhead = result;

	return;
}
