#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include "url.h"


static gchar *STRNDUP(const gchar *s, const gint len);

url_struct *URLNew(void);
url_struct *URLNewWithValues(
	const url_flags flags,
	const gchar *protocol,
	const gchar *user,
	const gchar *password,
	const gchar *host,
	const gint port,
	const gchar *path,
	const gchar *path_arg
);
url_struct *URLNewFromURLString(const gchar *url_str);
url_struct *URLCopy(const url_struct *url);
void URLDelete(url_struct *url);

guint8 *URLEncode(GList *url_list, gint *buf_len);
GList *URLDecode(const guint8 *buf, const gint buf_len);
GList *URLDecodeString(const gchar *s);

gchar *URLTokenize(const gchar *url_str);
gchar *URLDetokenize(const gchar *url_str);


#define URLPATHISABSOLUTE(s)	(((s) != NULL) ? (*(s) == '/') : FALSE)

#define ATOI(s)         (((s) != NULL) ? atoi(s) : 0)
#define ATOL(s)         (((s) != NULL) ? atol(s) : 0)
#define ATOF(s)         (((s) != NULL) ? atof(s) : 0.0f)
#define STRDUP(s)       (((s) != NULL) ? g_strdup(s) : NULL)

#define MAX(a,b)        (((a) > (b)) ? (a) : (b))
#define MIN(a,b)        (((a) < (b)) ? (a) : (b))
#define CLIP(a,l,h)     (MIN(MAX((a),(l)),(h)))
#define STRLEN(s)       (((s) != NULL) ? strlen(s) : 0)
#define STRISEMPTY(s)   (((s) != NULL) ? (*(s) == '\0') : TRUE)


/*
 *	Returns a coppied string of the specified string segment.
 *
 *	No more than len bytes from string s will be coppied.
 *
 *	The returned string may be shorter if a null terminating byte
 *	is encountered in s before len bytes is reached.
 */
static gchar *STRNDUP(const gchar *s, const gint len)
{
	const gchar *src, *src_end;
	gchar *tar, *s2 = ((s != NULL) && (len > 0)) ?
	    (gchar *)g_malloc((len + 1) * sizeof(gchar)) : NULL;
	if(s2 == NULL)
	    return(NULL);

	/* Iterate through source string and copy each byte to the
	 * target string while not exceeding the specified length
	 */
	src = s;
	src_end = (const gchar *)(src + len);
	tar = s2;
	while(src < src_end)
	{
	    /* End of source string encountered before the specified
	     * length?
	     */
	    if(*src == '\0')
	    {
		const gint tar_len = (gint)(tar - s2);

		/* Null terminate the target string, shorten its
		 * allocation, and then return
		 */
		*tar = '\0';
		return(g_realloc(s2, (tar_len + 1) * sizeof(gchar)));
	    }

	    *tar++ = *src++;
	}

	*tar = '\0';

	return(s2);
}


/*
 *	Creates a new URL.
 */
url_struct *URLNew(void)
{
	return(URL(g_malloc0(sizeof(url_struct))));
}

/*
 *	Creates a new URL from the specified values.
 */
url_struct *URLNewWithValues(
	const url_flags flags,
	const gchar *protocol,
	const gchar *user,    
	const gchar *password,
	const gchar *host,
	const gint port,
	const gchar *path,
	const gchar *path_arg
)
{
	url_struct *url = URLNew();
	if(url == NULL)
	    return(NULL);

	url->flags = flags;
	url->protocol = STRDUP(protocol);
	url->user = STRDUP(user);
	url->password = STRDUP(password);
	url->host = STRDUP(host);
	url->port = port;
	url->path = STRDUP(path);
	url->path_arg = STRDUP(path_arg);

	return(url);
}

/*
 *	Creates a new URL from the specified URL string.
 */
url_struct *URLNewFromURLString(const gchar *url_str)
{
	const gchar *s, *s2, *path;
	gchar *sp;
	url_struct *url = URLNew();
	if(url == NULL)
	    return(NULL);

	if(STRISEMPTY(url_str))
	    return(url);

	/* Begin parsing the URL string */
	s = url_str;

	/* If the URL string starts with a '/' then assume it only
	 * contains a path (without names or arguments)
	 */
	if(*s == '/')
	{
	    url->protocol = STRDUP("file");
	    url->path = STRDUP(s);
	    return(url);
	}

	/* Protocol */
	for(s2 = s; *s2 != '\0'; s2++)
	{
	    if(*s2 == ':')
		break;
	}
	url->protocol = STRNDUP(s, (gint)(s2 - s));

	/* Seek s past the protocol deliminator characters */
	if(*s2 == ':')
	    s2++;
	if(*s2 == '/')
	    s2++;
	if(*s2 == '/')
	    s2++;
	s = s2;

	/* Path
	 *
	 * Now s is past the protocol deliminator characters, seek
	 * path to the start of the path or marker, if there is no
	 * path or marker then path will point ot the end of the URL
	 * string
	 */
	for(path = s; *path != '\0'; path++)
	{
	    /* Start of path or marker reached? */
	    if((*path == '/') || (*path == '#'))
		break;
	}

	/* User & Password
	 *
	 * Check if there is a User & Password deliminator character
	 *
	 * s2 will point to the end of the User & Password segment if
	 * there is a User & Password or the end of the URL string
	 * if there isn't any
	 */
	for(s2 = s; *s2 != '\0'; s2++)
	{
	    if(*s2 == '@')
		break;
	}
	/* Is there a User & Password? */
	if(s2 < path)
	{
	    const gchar *s3;

	    /* Seek s3 to the password deliminator character or the end
	     * of the URL string if there isn't any
	     */
	    for(s3 = s; *s3 != '\0'; s3++)
	    {
		if(*s3 == ':')
		    break;
	    }
	    /* Is there a password? */
	    if(s3 < s2)
	    {
		url->user = STRNDUP(s, (gint)(s3 - s));
		s = (const gchar *)(s3 + 1);
		url->password = STRNDUP(s, (gint)(s2 - s));
	    }
	    else
	    {
		url->user = STRNDUP(s, (gint)(s2 - s));
	    }

	    /* Seek s past the User & Password segment and the
	     * deliminator character
	     */
	    s = (const gchar *)(s2 + 1);
	}

	/* Host & Port
	 *
	 * If s is not yet at the start of the path or marker then we
	 * can assume there is a Host & Port
	 */
	s2 = path;
	if(s < s2)
	{
	    const gchar *s3;

	    /* Seek s3 to the port deliminator character or the end of
	     * the URL string if there isn't any
	     */
	    for(s3 = s; *s3 != '\0'; s3++)
	    {
		if(*s3 == ':')
		    break;
	    }
	    /* Is there a port? */
	    if(s3 < s2)
	    {
		url->host = STRNDUP(s, (gint)(s3 - s));
		s = (const gchar *)(s3 + 1);
		url->port = ATOI(s);
	    }
	    else
	    {   
		url->host = STRNDUP(s, (gint)(s2 - s));
		url->port = 0;
	    }

	    /* Seek past Host & Port */
	    s = s2;
	}

	/* s should now be at the start of the path or marker segment */

	/* Path or Marker
	 *
	 * Check if s starts with the marker character
	 */
	if(*s == '#')
	{
	    s++;	/* Seek past marker character */
	    url->path = STRDUP(s);
	    url->flags |= URL_PATH_RELATIVE;
	}
	else if(*s == '/')
	{
	    url->path = STRDUP(s);
	}

	/* Path Argument
	 *
	 * Seek sp to the start of the path that was obtained above, if
	 * there is an argument then the argument will be removed from
	 * url->path and stored in url->path_arg
	 */
	sp = url->path;
	if(sp != NULL)
	{
	    while(*sp != '\0')
	    {
		/* Escape character? */
		if(*sp == '\\')
		{
		    sp++;	/* Seek past escape character */
		    if(*sp == '\0')
			break;
		}
		/* Start of argument? */
		else if(*sp == '?')
		{
		    const gint path_len = (gint)(sp - url->path_arg);

		    /* Get argument (not including the argument
		     * deliminator)
		     */
		    url->path_arg = STRDUP(sp + 1);

		    /* Null terminate the path at the argument character
		     * and reduce its allocation
		     */
		    *sp = '\0';
		    url->path = (gchar *)g_realloc(
			url->path,
			(path_len + 1) * sizeof(gchar)
		    );

		    break;
		}
		sp++;
	    }
	}

	/* Check if there is a name in the path */
	sp = url->path;
	if(sp != NULL)
	{
	    while(*sp != '\0')
	    {
		/* Escape character? */
		if(*sp == '\\')
		{
		    sp++;	/* Seek past escape character */
		    if(*sp == '\0')
			break;
		}
		/* Start of name? */
		else if(*sp == '#')
		{
		    const gint path_len = (gint)(sp - url->path_arg);

#if 0
/* TODO get name */
		    /* Get name (not including the name deliminator) */
		    url->path_name = STRDUP(sp + 1);
#endif

		    /* Null terminate the path at the name character
		     * and reduce its allocation
		     */
		    *sp = '\0';
		    url->path = (gchar *)g_realloc(
			url->path,
			(path_len + 1) * sizeof(gchar)
		    );
		      
		    break;    
		}
		sp++;
	    }
	}    


	return(url);
}

/*
 *	Coppies the URL.
 */
url_struct *URLCopy(const url_struct *url)
{
	return(URLNewWithValues(
	    url->flags,
	    url->protocol,
	    url->user,
	    url->password,
	    url->host,
	    url->port,
	    url->path,
	    url->path_arg
	));
}

/*
 *	Deletes the URL.
 */
void URLDelete(url_struct *url)
{
	if(url == NULL)
	    return;

	g_free(url->protocol);
	g_free(url->user);
	g_free(url->password);
	g_free(url->host);
	g_free(url->path);
	g_free(url->path_arg);
	g_free(url);
}


/*
 *	Encodes the specified list of URLs into a buffer of null byte
 *	separated URL strings.
 */
guint8 *URLEncode(GList *url_list, gint *buf_len)
{
	gint len = 0, cur_pos, url_len;
	const gchar	*protocol,
			*user,
			*password,
			*host,
			*path;
	gchar *url_str, port_str[40];
	guint8 *buf = NULL, *buf_ptr;
	GList *glist;
	const url_struct *url;

	if(buf_len != NULL)
	    *buf_len = len;

	if(url_list == NULL)
	    return(buf);

	/* Iterate through each URL */
	for(glist = url_list;
	    glist != NULL;
	    glist = g_list_next(glist)
	)
	{
	    url = URL(glist->data);
	    if(url == NULL)
		continue;

	    /* Get the URL values and use defaults as needed */
	    protocol = (url->protocol != NULL) ?
			url->protocol : "file";
	    user = url->user;
	    password = url->password;
	    host = (url->host != NULL) ? url->host : "";
	    if(url->port > 0)
		g_snprintf(
		    port_str, sizeof(port_str),
		    ":%i",
		    url->port
		);
	    else
		*port_str = '\0';
	    path = url->path;

	    /* If there is no path specified then it is not possible
	     * to encode this URL
	     */
	    if(STRISEMPTY(path))
		continue;

	    /* Generate the URL string
	     *
	     * Note: If path is not an absolute path then a '#'
	     * character will be used to deliminate the host address
	     * and path
	     */
	    if(!STRISEMPTY(user) && !STRISEMPTY(password))
		url_str = g_strconcat(
		    protocol,
		    "://",
		    user,
		    ":",
		    password,
		    "@",
		    host,
		    port_str,
		    URLPATHISABSOLUTE(path) ? "" : "#",
		    path,
		    NULL
		);
	    else if(!STRISEMPTY(user))
		url_str = g_strconcat(
		    protocol,
		    "://",
		    user,
		    "@",
		    host,
		    port_str,
		    URLPATHISABSOLUTE(path) ? "" : "#",
		    path,
		    NULL
		);
	    else
		url_str = g_strconcat(
		    protocol,
		    "://",
		    host,
		    port_str,
		    URLPATHISABSOLUTE(path) ? "" : "#",
		    path,
		    NULL
		);

	    if(url->path_arg != NULL)
	    {
		gchar *s = g_strconcat(url_str, "?", url->path_arg, NULL);
		g_free(url_str);
		url_str = s;
	    }

	    /* Calculate length of this url string (including the null
	     * terminating character) and increase buffer allocation to
	     * hold the entire url string plus one null character as a
	     * deliminator
	     */
	    cur_pos = len;			/* Record current buffer index */
	    url_len = STRLEN(url_str) + 1;	/* Length of URL string plus null char */
 	    len += url_len;			/* Increase total buffer length */

	    /* Increase the buffer allocation */
	    buf = (guint8 *)g_realloc(buf, len * sizeof(guint8));
	    if(buf == NULL)
	    {
		g_free(url_str);
		len = 0;
		break;
	    }

	    /* Get the pointer to the position of the current buffer
	     * segment
	     */
	    buf_ptr = (guint8 *)(buf + cur_pos);

	    /* Copy the URL to the current buffer segment, including
	     * the null character as a deliminator
	     */
	    memcpy(buf_ptr, url_str, url_len * sizeof(guint8));

	    g_free(url_str);
	}

	/* Update returns */
	if(buf_len != NULL)
	    *buf_len = len;

	return(buf);
}

/*
 *	Decodes the specified buffer containing null byte separated URL
 *	strings into a list of URLs.
 */
GList *URLDecode(const guint8 *buf, const gint buf_len)
{
	gint url_len;
	gchar *url_str;
	const guint8 *buf_ptr, *buf_end;
	GList *url_list = NULL;
	url_struct *url;

	if((buf == NULL) || (buf_len <= 0))
	    return(url_list);

	/* Iterate through buffer */
	buf_ptr = buf;
	buf_end = buf_ptr + buf_len;
	while(buf_ptr < buf_end)
	{
	    /* Make a copy of the current URL string segment */
	    url_str = STRNDUP(
		(const gchar *)buf_ptr, (gint)(buf_end - buf_ptr)
	    );
	    if(url_str == NULL)
		break;

	    /* Get the length of this URL string segment */
	    url_len = STRLEN(url_str);

	    /* Create a new URL from this URL string segment and
	     * append it to the list
	     */
	    url = URLNewFromURLString(url_str);
	    url_list = g_list_append(url_list, url);

	    /* Delete this URL string segment */
	    g_free(url_str);

	    /* Seek to the next URL string segment */
	    buf_ptr += url_len + 1;
	}

	return(url_list);
}


/*
 *	Decodes the specified string containing a space separated list
 *	of URL strings into a list of URLs.
 */
GList *URLDecodeString(const gchar *s)
{
	gchar *url_str;
	const gchar *s_next;
	GList *url_list = NULL;
	url_struct *url;

	if(s == NULL)
	    return(url_list);

	/* Iterate through string */
	while(*s != '\0')
	{
	    /* Seek s past the deliminator(s) */
	    while((*s == ' ') || (*s == '\t'))
		s++;

	    /* Seek s_next to the next deliminator or end of string */
	    for(s_next = s; *s_next != '\0'; s_next++)
	    {
		if((*s_next == ' ') || (*s_next == '\t'))
		    break;
	    }

	    /* Make a copy of the current URL string segment */
	    url_str = STRNDUP(s, (gint)(s_next - s));
	    if(url_str == NULL)
		break;

	    /* Create a new URL and append it to the list */
	    url = URLNewFromURLString(url_str);
	    url_list = g_list_append(url_list, url);

	    /* Delete the copy of the URL string segment */
	    g_free(url_str);

	    /* Seek s to the next deliminator (but not past it) */
	    s = s_next;
	}

	return(url_list);
}


/*
 *	Returns a copy of the tokenized URL string.
 */
gchar *URLTokenize(const gchar *url_str)
{
	gint len;
	const gchar *src;
	gchar *s2, *tar;

	if(url_str == NULL)
	    return(NULL);

	len = STRLEN(url_str);
	s2 = (gchar *)g_malloc((len + 1) * sizeof(gchar));
	if(s2 == NULL)
	    return(NULL);

	src = url_str;
	tar = s2;
	while(*src != '\0')
	{
	    if(((*src >= '0') && (*src <= '9')) ||
	       ((*src >= 'A') && (*src <= 'Z')) ||
	       ((*src >= 'a') && (*src <= 'z'))
	    )
	    {
		*tar++ = *src++;
	    }
	    else
	    {
		const gint i = (gint)(tar - s2);
		gchar h_str[8];

		len += 3;
		s2 = (gchar *)g_realloc(s2, (len + 1) * sizeof(gchar));
		if(s2 == NULL)
		    return(NULL);

		tar = s2 + i;

		g_snprintf(h_str, sizeof(h_str), "%%%.2X", *src++);
		*tar++ = h_str[0];
		*tar++ = h_str[1];
		*tar++ = h_str[2];
	    }
	}
	*tar = '\0';

	return(s2);
}

/*
 *	Returns a copy of the detokenizes URL string.
 */
gchar *URLDetokenize(const gchar *url_str)
{
	gint len;
	const gchar *src;
	gchar *s2, *tar;

	if(url_str == NULL)
	    return(NULL);

	len = STRLEN(url_str);
	s2 = (gchar *)g_malloc((len + 1) * sizeof(gchar));
	if(s2 == NULL)
	    return(NULL);

	src = url_str;
	tar = s2;
	while(*src != '\0')
	{
	    if(*src == '%')
	    {
		src++;
		if(*src == '\0')
		    break;
		else if((*src >= '0') && (*src <= '9'))
		    *tar = (*src - '0') * 16;
		else if((*src >= 'A') && (*src <= 'F'))
		    *tar = (*src - 'A' + 10) * 16;
		else if((*src >= 'a') && (*src <= 'f'))
		    *tar = (*src - 'a' + 10) * 16;
		else
		    *tar = 0x00;

		src++;
		if(*src == '\0') 
		    break;
		else if((*src >= '0') && (*src <= '9'))
		    *tar += *src - '0';
		else if((*src >= 'A') && (*src <= 'F'))
		    *tar += *src - 'A' + 10;
		else if((*src >= 'a') && (*src <= 'f'))
		    *tar += *src - 'a' + 10;

		src++;
		tar++;
	    }
	    else
	    {
		*tar++ = *src++;
	    }
	}
	*tar = '\0';

	return(s2);
}
