/*
 * 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: client.c,v 1.27 2002/11/21 14:36:27 alphix Exp $
 *
 * This file contains most of the functions that are used by the child
 * processes to interact with clients (read request, parse request 
 * and act accordingly).
 */


#include <config.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>
#include <sys/types.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>

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

/* How many bytes to send before it's time for to send ShoutCast metadata */
static int bytestometa = BYTESBETWEENMETA;

/*
 * Prepares data to be sent to the client.
 *
 * Arguments: buf - the buffer of size NETBUFFSIZE to write data to
 *            end - the position in the stream which is considered EOF
 *            from - the stream to read data from
 *
 * Returns: the amount of data which has been written to buf
 */
static int 
preparedata(char *buf, long end, FILE *from) 
{
	long current;
	int diff;
	int amount;
	
	if(end > 0) {
		current = ftell(from);
		if(current < 0)
			die("ftell()\n");
		diff = (int)(end - current);
		if(current == end)
			return(0);
	} else {
		diff = NETBUFFSIZE;
	}

	if(feof(from))
		return 0;

	amount = fread(buf, sizeof(char), min(diff, NETBUFFSIZE), from);
	
	if(ferror(from))
		die("read()\n");
	
	return amount;
}


/*
 * Sends data to the client while inserting ShoutCast type metadata at the
 * proper places.
 *
 * Arguments: cconf - session information
 *            buf - buffer to read data from
 *            amount - amount of data to read from buf
 *            playing - the file that is currently playing
 *
 * Returns: void
 */
static void 
senddata(struct client_config *cconf, char *buf, int amount, mp3entry *playing) 
{
	int towrite;
	int written = 0;

	if(!MODE_ISSET(MODE_METADATA)) {
		if(fwrite(buf, sizeof(char), amount, cconf->stream) != amount)
			die("Error writing to client\n");
	} else {
		while(written < amount) {
			towrite = min(amount - written, bytestometa);
			if(fwrite((buf + written), sizeof(char), towrite, cconf->stream) != towrite)
				die("Error writing to client\n");
			bytestometa -= towrite;
			written += towrite;
			if(bytestometa == 0) {
				if(writemetadata(cconf->stream, playing, &cconf->metadata))
					bytestometa = BYTESBETWEENMETA;
				else
					die("Error writing to client\n");
			}
		}
	}
}


/*
 * Positions the stream at the correct start and decides where to stop.
 *
 * Arguments: cconf - session information
 *            stream - the stream to position
 *            entry - the file that is about to be played
 *
 * Returns: the offset where to stop
 */
static long 
setoffsets(struct client_config *cconf, FILE *stream, mp3entry *entry) 
{
	long start;
	long end;

	if(cconf->startpos > 0 && cconf->startpos < entry->filesize)
		start = cconf->startpos;
	else if(!MODE_ISSET(MODE_SINGLE) && entry->id3v2len)
		start = entry->id3v2len;
	else
		start = 0;
	fseek(stream, start, SEEK_SET);

	if(cconf->endpos > 0 && cconf->endpos < entry->filesize)
		end = cconf->endpos;
	else if(HASID3V1(entry) && end > entry->id3v1len)
		end = entry->filesize - entry->id3v1len;
	else
		end = entry->filesize;

	debug(4, "Start offset is %li, end offset is %li\n", start, end);
	return end;
}


/*
 * Plays a file to the client, including metadata if that is reqested.
 *
 * Arguments: cconf - session information
 *            entry - the entry to play
 *
 * Returns: void
 */
static void 
playfile(struct client_config *cconf, mp3entry *entry) 
{
	FILE *file;
	char buf[NETBUFFSIZE];
	int amount;
	long end;
	
	cconf->metadata = TRUE;
	debug(1, "Playing file %s\n", entry->path);
	sendstatusmsg(cconf->statussock, "%d:Playing file %s", 
		      getpid(), entry->path);

	if(gconf.filter != NULL) {
		if((file = popen(replacevariables(gconf.filter, 
						  cconf, NULL), "r")) == NULL)
			die("popen()\n");
		end = -1;
	} else {
		if ((file = fopen(entry->path,"r")) == NULL)
			die("fopen()\n");
		end = setoffsets(cconf, file, entry);
	}
	

	while((amount = preparedata(buf, end, file)))
		senddata(cconf, buf, amount, entry);
	
	if(gconf.filter != NULL)
		pclose(file);
	else
		fclose(file);
}


/*
 * Plays a range of different files to the client.
 *
 * Arguments: cconf - session information
 *            min - the lowest allowed index in the filetree of the range 
 *                  of files to play
 *            max - the highest allowed index in the filetree of the range
 *                  of files to play
 *            order - should the files be played in order or randomly?
 *
 * Returns: void
 */
static void 
playrange(struct client_config *cconf, int min, int max, bool order)
{
	int i;
	int toplay;
	mp3entry *current;
	int offset;
	int numentries;
	int numplayed;
	char *played = NULL; /* bitpattern */
	int playedsize;

	if(order) {
		toplay = max;
	} else {
		numplayed = 0;
		numentries = max - min + 1;
		playedsize = numentries/8 + 1;

		srand((unsigned int)time(NULL) + (unsigned int)getpid());

		if((played = malloc(playedsize)) == NULL)
			die("malloc\n");
		memset(played, 0, playedsize);
	}

	debug(1, "In playrange with range %i - %i and order %i\n",  
	      min,  max,  order);
	
	while(TRUE) {
		if(order) {
			toplay == max ? toplay = min : toplay++;
		} else {
			/* Check if all songs have been played */
			if(numentries - numplayed == 0) {
				numplayed = 0;
				memset(played, 0, playedsize);
			}
			
			/* Get random offset within songs */
			offset = (int)((float)(numentries - numplayed) * 
				       rand() / (RAND_MAX+1.0));

			/* Loop till we find an unplayed song at that offset */
			for(i = 0; i < numentries; i++) {

				if(((played[i/8] >> i%8) & 1) == 0)
					offset--;

 				if(offset < 0) {
					played[i/8] |= (1 << i%8);
					break;
				}
				
			}
			
			numplayed++;
			toplay = min + i;

			for(i = 0; i < playedsize; i++)
				debug(5, "Offset %i: %x\n", i*8, (unsigned int)played[i]);
		}

		/* HACK: findentrybyindex shouldn't modify second argument */
		i = toplay;
		current = findentrybyindex(root, &i);
		if(!current)
			die("Couldn't find entry with index %i\n", toplay);
		else
			debug(1, "Going to play %s - index %i - order %i\n", current->name, toplay, order);
		playfile(cconf, current);
	}

	if(played)
		free(played);
}


/*
 * Creates a HTML page listing the available files and directories and sends
 * it to the client.
 *
 * Arguments: cconf - session information
 *            base - the base where to start the list from, this file and
 *                   all below it will be listed
 *
 * Returns: void
 */
static void 
createhtml(struct client_config *cconf, mp3entry *base)
{
	int i = 0;
	mp3entry * tmp = base;
	
	if(tmp == NULL || cconf->stream == NULL){
		debug(3, "createhtml: base or output stream NULL - exiting");
		return;
	}
	
	fprintf(cconf->stream, "%s", replacevariables(gconf.htmlheader, cconf, NULL));
	while(tmp != NULL) {
		fprintf(cconf->stream, "%s", replacevariables(gconf.htmlmiddle, cconf, tmp));
		tmp = tmp->sibling;
	}
	fprintf(cconf->stream, "%s", replacevariables(gconf.htmlfooter, cconf, NULL));
}


/*
 * Creates a HTML page listing interesting info to the client
 *
 * Arguments: cconf - session information
 *
 * Returns: void
 */
static void 
createinfohtml(struct client_config *cconf)
{
	int i;
	int clientno = 0;

	if(cconf->stream == NULL){
		debug(3, "createinfohtml: output stream NULL - exiting");
		return;
	}
	
	fprintf(cconf->stream, INFOHEADER, gconf.servername, gconf.servername);

	fprintf(cconf->stream, "<tr><td colspan=\"2\"><b>SERVER STATUS</b></td></tr>\n");
	fprintf(cconf->stream, "<tr><td>Name</td><td>%s</td></tr>\n", gconf.servername);
	fprintf(cconf->stream, "<tr><td>Address</td><td>%s</td></tr>\n", gconf.serveraddress);
	fprintf(cconf->stream, "<tr><td>Port</td><td>%d</td></tr>\n", gconf.port);
	fprintf(cconf->stream, "<tr><td>Max clients</td><td>%i</td></tr>\n", gconf.max_clients);

	fprintf(cconf->stream, "<tr><td colspan=\"2\"><b>CLIENT STATUS</b></td></tr>\n");

	if(gconf.inetd) {
		fprintf(cconf->stream, "<tr><td colspan=\"2\">In inetd mode, no status available</td></tr>\n");
	} else if(gconf.trace) {
		fprintf(cconf->stream, "<tr><td colspan=\"2\">In trace mode, no status available</td></tr>\n");
	} else {
		for(i = 0; i < gconf.max_clients; i++) {
			if(childarray[i].childpid == 0)
				continue;
			clientno++;
			fprintf(cconf->stream, "<tr><td>%i - %s</td><td>%s</td></tr>\n", 
				clientno, childarray[i].client, childarray[i].status);
		}
		if(clientno == 0)
			fprintf(cconf->stream, "<tr><td colspan=\"2\">None</td></tr>\n");
	}
	
	fprintf(cconf->stream, INFOFOOTER);
}


/*
 * Creates a M3U (playlist) file and sends it to the client.
 *
 * Arguments: cconf - session information
 *            base - the base where to start the list from, this file and
 *                   all below it will be listed
 *            recursive - should all subdirs be included as well?
 *
 * Returns: void
 */
static void
createm3u(struct client_config *cconf, mp3entry *base, bool recursive)
{
	mp3entry * tmp = base;
	char *dir = findpathbyentry(root, base, "");
	char *vpath;

	if(dir == NULL || tmp == NULL || cconf->stream == NULL)
		return;
	dir = dirname(dir);
	
	while(tmp != NULL) {
		if(IS_DIR(tmp)) {
			if(recursive && tmp->child)
				createm3u(cconf, tmp->child, recursive);
		} else if(vpath = findpathbyentry(root, tmp, "")) {
			fprintf(cconf->stream, "#EXTINF:%i,%s\n", 
				tmp->length, tmp->title);
			fprintf(cconf->stream, "http://%s:%d%s\n", 
				gconf.serveraddress, gconf.port, vpath);
			free(vpath);
		}
		tmp = tmp->sibling;
	}
}


/*
 * Checks if authentication is needed and if username/password is correct
 *
 * Arguments: cconf - Client configuration
 *
 * Returns: TRUE if client is allowed to connect, FALSE otherwise
 */
static bool 
check_authentication(struct client_config *cconf) 
{
	if(gconf.username == NULL || gconf.password == NULL)
		return(TRUE);
	
	if(cconf->username == NULL || 
	   cconf->password == NULL ||
	   strcmp(cconf->username, gconf.username) || 
	   strcmp(cconf->password, gconf.password)) {
		/* No user/pass or wrong user/pass given */
		fprintf(cconf->stream, AUTHMSG, gconf.servername);
		fflush(cconf->stream);
		return(FALSE);
	} else {
		return(TRUE);
	}
}

/*
 * Reads the client request and acts accordingly.
 *
 * Arguments: conn - the file descriptor of the socket which the client
 *                   connected to
 *            udpsock - the socket to send status messages to
 *
 * Returns: the status that the client should exit with 
 *          (i.e. EXIT_SUCCESS or EXIT_FAILURE)
 */
int 
handleclient(int conn, int udpsock) 
{
	int min;
	int max;
	/* Client and session configuration */
	struct client_config *cconf = NULL;
	
	/* Prepare configuration struct */
	cconf = malloc(sizeof(struct client_config));
	if(cconf == NULL)
		die("Malloc failed\n");
	memset(cconf, 0, sizeof(struct client_config));
	cconf->statussock = udpsock;

	/* Send initial status message to main server */
	sendstatusmsg(cconf->statussock, "%d:connecting", getpid());

	/* Read and parse client request */
	if ((cconf->stream = fdopen(conn, "r+")) == NULL || 
	    !readrequest(cconf)) {
		debug(1, "Error while reading client request\n");
		fclose(cconf->stream);
		free(cconf);
		return(EXIT_FAILURE);
	}
	debug(1, "Requested path %s\n", cconf->requestpath);
	cconf->mp3base = findentrybypath(root, cconf->requestpath);
	/* FIXME: Should give a 403 instead */
	if(cconf->mp3base == NULL)
		die("Incorrect file/dir requested\n");

	if(!check_authentication(cconf))
		die("Incorrect or no credentials specified\n");

	if(MODE_ISSET(MODE_INDEX)) {
		debug(1, "Entering HTML mode\n");
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending HTML data", getpid());
		fprintf(cconf->stream, HTTPSERVMSG, AMPLE_VERSION);
		fflush(cconf->stream);
		createhtml(cconf, cconf->mp3base);

	} else if(MODE_ISSET(MODE_RM3U)) {
		debug(1, "Entering recursive M3U mode\n");
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending recursive M3U data", getpid());
		fprintf(cconf->stream, M3USERVMSG, AMPLE_VERSION);
		fflush(cconf->stream);
		fprintf(cconf->stream, "#EXTM3U\n");
		createm3u(cconf, cconf->mp3base, TRUE);

	} else if(MODE_ISSET(MODE_M3U)) {
		debug(1, "Entering non-recursive M3U mode\n");
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending non-recursive M3U data", getpid());
		fprintf(cconf->stream, M3USERVMSG, AMPLE_VERSION);
		fflush(cconf->stream);
		fprintf(cconf->stream, "#EXTM3U\n");
		createm3u(cconf, cconf->mp3base, FALSE);
		
	} else if(MODE_ISSET(MODE_PARTIAL)) {
		debug(1, "Entering MP3-Partial mode\n");
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending partial MP3", getpid());
		if(cconf->endpos = 0 || cconf->endpos > cconf->mp3base->filesize)
			fprintf(cconf->stream, PARTIALSERVMSG, AMPLE_VERSION, 
				cconf->startpos, cconf->mp3base->filesize, 
				cconf->mp3base->filesize);
		else
			fprintf(cconf->stream, PARTIALSERVMSG, AMPLE_VERSION, 
				cconf->startpos, cconf->endpos, 
				cconf->mp3base->filesize);
		fflush(cconf->stream);
		playfile(cconf, cconf->mp3base);

	} else if(MODE_ISSET(MODE_SINGLE)) {
		debug(1, "Entering MP3-Single mode\n");
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending single MP3", getpid());
		fprintf(cconf->stream, SINGLESERVMSG, AMPLE_VERSION, 
			cconf->mp3base->filesize);
		fflush(cconf->stream);
		playfile(cconf, cconf->mp3base);

	} else if (MODE_ISSET(MODE_METADATA)) {
		getrange(cconf->mp3base, gconf.recursive, &min, &max);
		debug(1, "Entering MP3-Metadata mode (range %i - %i)\n", 
		      min, max);
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending MP3's and metadata", getpid());
		fprintf(cconf->stream, SHOUTSERVMSG, AMPLE_VERSION, 
			gconf.servername, BYTESBETWEENMETA);
		fflush(cconf->stream);
		playrange(cconf, min, max, gconf.order);

	} else if(MODE_ISSET(MODE_INFO)) {
		debug(1, "Entering INFO mode\n");
		sendstatusmsg(cconf->statussock, 
			      "%d:Sending INFO data", getpid());
		fprintf(cconf->stream, HTTPSERVMSG, AMPLE_VERSION);
		fflush(cconf->stream);
		createinfohtml(cconf);

	} else {
		getrange(cconf->mp3base, gconf.recursive, &min, &max);
		debug(1, "Entering MP3-Basic mode (range %i - %i)\n", 
		      min, max);
		sendstatusmsg(cconf->statussock, "%d:Sending MP3's", getpid());
		MODE_SET(MODE_BASIC);
		fprintf(cconf->stream, BASICSERVMSG, AMPLE_VERSION);
		fflush(cconf->stream);
		playrange(cconf, min, max, gconf.order);
	}

	fclose(cconf->stream);
	free(cconf);
	cleartree(&root);
	return(EXIT_SUCCESS);
}
