/*
 * Copyright (c) 2001-2003 Shiman Associates Inc. All Rights Reserved.
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

/* Some code taken from freedb.org's webpage. */
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "mas/mas_dpi.h"
#include "mas_cdrom_common.h"
#include "cdrom_int_device.h"



/* Formats the data for various reasons */
static void format_cddb_data(char *data)
{
	int i;


	if(!data) return;

	for(i = 0; i < strlen(data); i++)
	{
		/* Replace all '\n' '\r' with spaces */
		if(data[i] == '\n' || data[i] == '\r') data[i] = ' ';

		/* Replace all literal "\n" with ' \n' */
		if(data[i] == '\\' && data[i+1] == 'n')
		{
			data[i] = ' ';
			data[i+1] = '\n';
			i++;
		}
	}
}

/* Find a tag and it's data from a returned cddb response. Returns
 * a pointer to a new malloc()ed string. */
static char *get_cddb_tag_data(char *tag, char *response)
{
	char		*current_location = response;
	char		*tag_start;
	char		*data_start;
	char		*data_end;
	char		*data = NULL;


	masc_entering_log_level("Getting tag data from server response: get_cddb_tag_data()");

	/* The return statements will end this loop. */
	while(1)
	{
		/* There should be at least one instance of this tag */
		if((tag_start = strstr(current_location, tag)) == NULL)
			goto success;

		/* Find the beginning of the data */
		if((data_start = strchr(tag_start, '=')) == NULL)
			goto success;

		data_start++;
	
		/* Find the end of the data */
		if((data_end = strchr(data_start, '\n')) == NULL)
			goto success;
	
		/* Now put the string into the data buffer */
		if(data == NULL)
		{
			if((data = malloc(data_end - data_start + 2)) == NULL)
			{
				masc_log_message(MAS_VERBLVL_ERROR, "malloc returned NULL");
				goto failure;
			}
			data[0] = '\0';
		}
		else
		{
			if((data = realloc(data, strlen(data) + data_end - data_start + 2)) == NULL)
			{
				masc_log_message(MAS_VERBLVL_ERROR, "realloc returned NULL");
				goto failure;
			}
		}
		strncat(data, data_start, data_end - data_start + 1);

		current_location = data_end;
	}
	masc_exiting_log_level();

	goto success;
failure:
	if(data) free(data);
	data = NULL;
success:
	if(data) format_cddb_data(data);
	masc_exiting_log_level();
	return data;
}


/* Takes a string and escapes any cgi characters */
static int escape_cgi_chars(char *old_string, char **new_string)
{
	int	old_loc, new_loc;
	int	special_chars = 0;
	char	escaped_char[3];


	masc_entering_log_level("Escaping special chars in CGI string: escape_cgi_chars()");

	/* Find how many special chars are in the string */
	for(old_loc=0; old_loc<strlen(old_string); old_loc++)
		if(old_string[old_loc] <= '+' || old_string[old_loc] >= '{')
			special_chars++;

	/* Malloc a new string based on the number of characters to escape plus a null terminator
	 * Escaping characters adds two more characters. '+' = %2B */
	if((*new_string = (char*)malloc(strlen(old_string) + special_chars * 2 + 1)) == NULL)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "malloc() failed for new_string");
		masc_exiting_log_level();
		return 0;
	}

	/* Now escape all the characters */
	new_loc = 0;
	for(old_loc=0; old_loc<strlen(old_string); old_loc++)
		if(old_string[old_loc] <= '+' || old_string[old_loc] >= '{')
		{
			/* Replace the spaces since cddb servers don't handle them correctly. */
			if(old_string[old_loc] == ' ')
			{
				(*new_string)[new_loc] = '_';
				new_loc++;
			}
			else
			{
				(*new_string)[new_loc] = '%';
				new_loc++;
				sprintf(escaped_char, "%02x", (int)old_string[old_loc]);
				strcpy((*new_string)+new_loc, escaped_char);
				new_loc += 2;
			}
		}
		else
		{
			(*new_string)[new_loc] = old_string[old_loc];
			new_loc++;
		}

	/* Put a null terminator on the end of the string */
	(*new_string)[new_loc] = '\0';


	masc_exiting_log_level();

	return 1;
}

/* Generates the freedb id of the cd */
static int generate_cddb_id(struct cdrom_device *cd_dev)
{
	struct track_info	*tracks = cd_dev->tracks;
	int			tot_trks = cd_dev->number_of_tracks;
        int     		i, t = 0, n = 0;
	int			track_offset;


        /* For backward compatibility this algorithm must not change */
        for(i=0; i < tot_trks; i++)
	{
		track_offset = tracks[i].start_msf.minute * 60 + tracks[i].start_msf.second;
		
		while(track_offset > 0)
		{
			n += track_offset % 10;
			track_offset /= 10;
		}
	}
	

        t = ((tracks[tot_trks].start_msf.minute * 60) + tracks[tot_trks].start_msf.second) - ((tracks[0].start_msf.minute * 60) + tracks[0].start_msf.second);

        cd_dev->cddb_id = (n % 0xff) << 24 | t << 8 | tot_trks;


	return 1;
}

/* Sends a string to the cddb server and then mallocs a string for the
 * response. Be sure to free the string that is returned. Returns NULL
 * on error. */
static char *send_http_command(char *servername, char *command)
{
#define BUF_AMOUNT 1024
	struct sockaddr_in	cddb_server;
	struct hostent		*hostdata;
	int			fdSocket = -1;
	int			amount, recv_ret;
	char			*response = NULL;


	masc_entering_log_level("Sending HTTP request: send_http_command()");

	/* Get the server and connect to it. */
	if((hostdata = gethostbyname(servername)) == NULL)
	{
		masc_log_message(MAS_VERBLVL_WARNING, "gethostbyname failed for: %s", servername);
		goto failure;
	}

	memset(&cddb_server, 0, sizeof(cddb_server));
	cddb_server.sin_family = AF_INET;
	cddb_server.sin_addr.s_addr = ((struct in_addr *)hostdata->h_addr_list[0])->s_addr;
	cddb_server.sin_port = htons(80);

	if((fdSocket = socket(AF_INET, SOCK_STREAM, 0)) <= 0)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "socket creation failed: %s", strerror(errno));
		goto failure;
	}

	if(connect(fdSocket, (struct sockaddr *)&cddb_server, sizeof(struct sockaddr_in)) < 0)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "failed to connect socket: %s", strerror(errno));
		goto failure;
	}

	/* Send the string */
	if(send(fdSocket, command, strlen(command), 0) <= 0)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "failed to send data across socket: %s", strerror(errno));
		goto failure;
	}

	/* Malloc the buffer for the response */
	if((response = (char*)malloc(BUF_AMOUNT)) == NULL)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "malloc returned NULL");
		goto failure;
	}

	/* Get the response */
	amount = 0;
	do{
		recv_ret = recv(fdSocket, response+amount, BUF_AMOUNT, 0);

		if(recv_ret < 0)
		{
			masc_log_message(MAS_VERBLVL_ERROR, "failed to recv data: %s", strerror(errno));
			goto failure;
		}

		amount += recv_ret;
		/* if the maximum amount was recieved, realloc for more data */
		if(recv_ret == BUF_AMOUNT)
			if((response = realloc(response, amount + BUF_AMOUNT)) == NULL)
			{
				masc_log_message(MAS_VERBLVL_ERROR, "realloc returned NULL");
				goto failure;
			}

	}while(recv_ret > 0);


	/* Make sure the end it null terminated. */
	response[amount] = '\0';
	goto success;
failure:
	if(response) free(response);
	response = NULL;
success:
	if(fdSocket != -1) close(fdSocket);
	masc_exiting_log_level();
	return response;
#undef BUF_AMOUNT
}


/* Get the cddb info from across the network
 * Use the HTTP protocol. */
static int get_cddb_dbinfo(struct cdrom_device *cd_dev, char *servername, char *username)
{
	char	hello[2048];
	char	command[2048];
	char	group[128];
	char	tag[64];
	char	*response, *sub_resp, *begin, *end;
	char	*cgi_username, *cgi_appname;
	int	i;
	

	masc_entering_log_level("Getting CDDB info: get_cddb_dbinfo()");

	/***************
	 * Create the "hello" string.
	 ***************/
	if(!escape_cgi_chars(username, &cgi_username))
	{
		masc_exiting_log_level();
		return 0;
	}
	if(!escape_cgi_chars(MAS_NAME, &cgi_appname))
	{
		masc_exiting_log_level();
		return 0;
	}
	sprintf(hello, "&hello=%s+%s+%s+%d.%d.%d&proto=4 HTTP/1.0\r\n\r\n",
			cgi_username, "armstrong.shiman.com", cgi_appname,
			MAS_VERSION_MAJOR, MAS_VERSION_MINOR, MAS_VERSION_TEENY);
	free(cgi_username);
	free(cgi_appname);


	/***************
	 * Send the "query" command to the database
	 ***************/
	sprintf(command, "GET /~cddb/cddb.cgi?cmd=cddb+query+%08x+%d",
			cd_dev->cddb_id, cd_dev->number_of_tracks);
	/* Add the track offsets in frames */
	for(i=0; i<cd_dev->number_of_tracks; i++)
		sprintf(command+strlen(command), "+%d",
					cd_dev->tracks[i].start_msf.minute * 60 * 75 + 
					cd_dev->tracks[i].start_msf.second * 75 +
					cd_dev->tracks[i].start_msf.frame);
	/* Add the leadout offset in seconds and the "hello" string. */
	sprintf(command+strlen(command), "+%d%s",
					cd_dev->tracks[i].start_msf.minute * 60 +
					cd_dev->tracks[i].start_msf.second, hello);

	/* Now send off the "query" command */
	if((response = send_http_command(servername, command)) == NULL)
	{
		masc_exiting_log_level();
		return 0;
	}
	
	/**************
	 * Now create a "read" command. 
	 **************/
	/* Get the group that the track was found in. */
	if((sub_resp = strstr(response, "\r\n\r\n")) == NULL)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "Can not process syntax of http response");
		masc_exiting_log_level();
		return 0;
	}
	sub_resp += 4;
	if((begin = strchr(sub_resp, ' ')) == NULL)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "Can not process syntax of http response");
		masc_exiting_log_level();
		return 0;
	}
	begin++;
	if((end = strchr(begin, ' ')) == NULL)
	{
		masc_log_message(MAS_VERBLVL_ERROR, "Can not process syntax of http response");
		masc_exiting_log_level();
		return 0;
	}
	strncpy(group, begin, end-begin);
	group[end-begin] = '\0';
	free(response);

	/* Build the command string */
	sprintf(command, "GET /~cddb/cddb.cgi?cmd=cddb+read+%s+%08x%s",
			group, cd_dev->cddb_id, hello);
	if((response = send_http_command(servername, command)) == NULL)
	{
		masc_exiting_log_level();
		return 0;
	}


	/**************
	 * Store the cd info in the device.
	 **************/
	/* Get the title of the cd */
	cd_dev->cd_title = get_cddb_tag_data("DTITLE", response);

	/* Get the year of the cd */
	cd_dev->cd_year = get_cddb_tag_data("DYEAR", response);

	/* Get the genre of the cd */
	cd_dev->cd_genre = get_cddb_tag_data("DGENRE", response);

	/* Now fill in all the track titles */
	for(i = 0; i < cd_dev->number_of_tracks; i++)
	{
		sprintf(tag, "TTITLE%d", i);
		cd_dev->tracks[i].trackname = get_cddb_tag_data(tag, response);
	}

	/* Throw the rest of the data in misc in case the user wants it. */
	cd_dev->cd_misc_data = response;

	masc_exiting_log_level();

	return 1;
}


/* Gets the cddb info from a cddb database. */
int update_cddb_info(struct cdrom_device *cd_dev, char *username, char *servername)
{
	int			ret_val;
	int			i;



	masc_entering_log_level("Updating CDDB info: update_cddb_info()");

	/* First clear out all the existing cddb info */
	cd_dev->cddb_id = 0;	
	if(cd_dev->cd_genre) free(cd_dev->cd_genre);
	cd_dev->cd_genre = NULL;	
	if(cd_dev->cd_title) free(cd_dev->cd_title);
	cd_dev->cd_title = NULL;	
	if(cd_dev->cd_year) free(cd_dev->cd_year);
	cd_dev->cd_year = NULL;	
	if(cd_dev->cd_misc_data) free(cd_dev->cd_misc_data);
	cd_dev->cd_misc_data = NULL;	
	for(i=0; i<cd_dev->number_of_tracks; i++)
	{
		free(cd_dev->tracks[i].trackname);
		cd_dev->tracks[i].trackname = NULL;
	}
	cd_dev->number_of_tracks = 0;

	/* Make sure the cdinfo is updated */
	if(!mas_cdrom_update_status(cd_dev))
		goto failure;

	/* Grab the ID and store it */
	if(!generate_cddb_id(cd_dev))
		goto failure;

	/* Now get the cddb info from a cddb server if the server name is not NULL */
	if(!get_cddb_dbinfo(cd_dev, servername, username))
		goto failure;

	ret_val = 1;
	goto success;
failure:
	ret_val = 0;
success:
	masc_exiting_log_level();
	return ret_val;
}
