#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <glib.h>

#include "../include/string.h"
#include "../include/fio.h"
#include "../include/disk.h"

#include "edv_types.h"
#include "edv_mime_type.h"
#include "edv_mime_types_list.h"
#include "config.h"


void EDVMimeTypesListMediaTypesImport(
	const gchar *path,
	edv_mime_type_struct ***list, gint *total,
	const gint insert_index,
	const gboolean update, const gboolean only_newer,
	gint (*progress_cb)(gpointer, const gulong, const gulong),
	gpointer progress_data,
	void (*added_cb)(const gint, edv_mime_type_struct *, gpointer),
	gpointer added_data,
	void (*modified_cb)(const gint, edv_mime_type_struct *, gpointer),
	gpointer modified_data
);
void EDVMimeTypesListMediaTypesExport(
	const gchar *path,
	edv_mime_type_struct **list, const gint total,
	const gboolean include_read_only,
	gint (*progress_cb)(gpointer, const gulong, const gulong),
	gpointer progress_data
);


#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)

#define ISCR(c)		(((c) == '\n') || ((c) == '\r'))
#define ISEOF(c)	((int)(c) == EOF)


static gchar *G_STRCAT(gchar *s, const gchar *s2)
{
	if(s != NULL) {
	    if(s2 != NULL) {
		gchar *sr = g_strconcat(s, s2, NULL);
		g_free(s);
		s = sr;
	    }
	} else {
	    if(s2 != NULL)
		s = STRDUP(s2);
	    else
		s = STRDUP("");
	}
	return(s);
}

/*
 *	Imports the MIME Types from the Media Types file.
 *
 *	The path specifies the Media Types file.
 *
 *	The list and total specifies the pointers to the MIME Types     
 *	list.
 *
 *	The insert_index specifies the position at which to insert the
 *	MIME Types read from file to the list, or it can be -1 to
 *	append to the list.
 *
 *	If update is TRUE then if a MIME Type from the file already
 *	exists in the MIME Types list then it will be updated instead
 *	of a new MIME Type being added.
 *
 *	If only_newer is TRUE and update is TRUE then if a MIME Type
 *	from the file already exists in the MIME Types list then it
 *	will only be updated if the one from the file has a newer
 *	modified time.
 *
 *	If progress_cb is not NULL then it will be called during the
 *	operation to report the progress. If it returns non-zero then
 *	the operation will be aborted.
 *
 *	The progress_data specifies the user data which will be passed
 *	to the progress_cb function.
 *
 *	If added_cb is not NULL then it will be called after each new
 *	MIME Type is added to the list.
 *
 *	The added_data specifies the user data which will be passed to
 *	the added_cb function.
 *
 *	If modified_cb is not NULL then it will be called whenever an
 *	existing MIME Type in the MIME Types list is modified.
 */
void EDVMimeTypesListMediaTypesImport(
	const gchar *path,
	edv_mime_type_struct ***list, gint *total,
	const gint insert_index,
	const gboolean update, const gboolean only_newer,
	gint (*progress_cb)(gpointer, const gulong, const gulong),
	gpointer progress_data,
	void (*added_cb)(const gint, edv_mime_type_struct *, gpointer),
	gpointer added_data,
	void (*modified_cb)(const gint, edv_mime_type_struct *, gpointer),
	gpointer modified_data
)
{
	gboolean aborted = FALSE, c_literal, mt_exists;
	FILE *fp;
	gint	i, c,
		cur_insert_index = insert_index,
		mt_num = -1;
	gulong	file_size = 0l,
		file_last_modified = 0l;
	gchar	*s,
		type_str[1024],
		ext_str[2048];
	GList *mt_imported_list = NULL;
	edv_mime_type_struct *mt = NULL;
	struct stat stat_buf;

	if((list == NULL) || (total == NULL) || STRISEMPTY(path))
	    return;

	/* Open the Media Types file for reading */
	fp = fopen((const char *)path, "rb");
	if(fp == NULL)
	    return;

	/* Get file statistics */
	if(!fstat(fileno(fp), &stat_buf))
	{
	    file_size = stat_buf.st_size;
	    file_last_modified = stat_buf.st_mtime;
	}

	/* Report the initial progress */
	if(progress_cb != NULL)
	{
	    if(progress_cb(progress_data, 0l, file_size))
 		aborted = TRUE;
	}

	/* Begin parsing the Media Types format
	 *
	 * Each line is a newline terminated string with space
	 * separated values, example:
	 *
	 * application/x-png png mng
	 */

	while(!aborted)
	{
/* Gets the next character as c and updates c_literal, c_literal will
 * be set to TRUE if there was an escape character encountered
 */
#define GET_NEXT_CHAR_LITERAL	{	\
 /* Get next character */		\
 c = (gint)fgetc(fp);			\
 c_literal = FALSE;			\
					\
 /* Is it escaped? */			\
 if(c == '\\') {			\
  c = (gint)fgetc(fp);			\
  /* Skip characters that need to be	\
   * escaped and mark if the character	\
   * is literal				\
   */					\
  if(ISCR(c))				\
   c = (gint)fgetc(fp);			\
  else					\
   c_literal = TRUE;			\
 }					\
}

	    /* Get the first non-blank character of the current line */
	    GET_NEXT_CHAR_LITERAL
	    while(!ISEOF(c) && ISBLANK(c))
		GET_NEXT_CHAR_LITERAL

	    /* End of file? */
	    if(ISEOF(c))
	    {
		break;
	    }
	    /* Empty line? */
	    else if(ISCR(c))
	    {
		continue;
	    }
	    /* Comment? */
	    else if(!c_literal && (c == '#'))
	    {
		/* Seek to start of next line and continue */
		while(!ISEOF(c) && !ISCR(c))
		    GET_NEXT_CHAR_LITERAL
		continue;
	    }

	    /* Report progress */
	    if(progress_cb != NULL)
	    {
		if(progress_cb(
		     progress_data,
		     (gulong)ftell(fp), file_size
		))
		{
		    aborted = TRUE;
		    break;
		}
	    }

	    /* Begin fetching the type string, this is always the first
	     * field before the first blank deliminator character
	     */
	    s = type_str;
	    for(i = 0; i < sizeof(type_str); i++)
	    {
		if(!c_literal && ISBLANK(c))
		{
		    GET_NEXT_CHAR_LITERAL
		    break;
		}
		else if(ISEOF(c) || ISCR(c))
		{
		    break;
		}
		else
		{
		    *s++ = (gchar)c;
		    GET_NEXT_CHAR_LITERAL
		}
	    }
	    /* Null terminate the string */
	    if(i < sizeof(type_str))
		type_str[i] = '\0';
	    else
		type_str[sizeof(type_str) - 1] = '\0';


	    /* Begin fetching extensions up until the next newline
	     * character
	     */
	    s = ext_str;
	    for(i = 0; i < sizeof(ext_str); i++)
	    {
		if(ISEOF(c) || ISCR(c))
		{
		    break;
		}
		else
		{
		    *s++ = (gchar)c;
		    GET_NEXT_CHAR_LITERAL
		}
	    }
	    /* Null terminate the string */
	    if(i < sizeof(ext_str))
		ext_str[i] = '\0';
	    else
		ext_str[sizeof(ext_str) - 1] = '\0';


	    /* In case end of line has not been reached, seek until
	     * the next newline character is encountered
	     */
	    while(!ISEOF(c) && !ISCR(c))
		GET_NEXT_CHAR_LITERAL


	    /* Begin creating new imported MIME Type */

	    /* Reset contexts */
	    mt_num = -1;
	    mt = NULL;
	    mt_exists = FALSE;

	    /* Check if the MIME Type already exists? */
	    if(update)
	    {
		if(!STRISEMPTY(type_str))
		{
		    /* Check if this MIME Type is already in the list
		     * so that no duplicate MIME Type will be created
		     */
		    mt = EDVMimeTypesListMatchType(
			*list, *total, &mt_num, type_str, FALSE
		    );
		    if(mt != NULL)
		    {
			GList *glist;

			/* MIME Type already exists */
			mt_exists = TRUE;

			/* Check if the MIME Type was already defined
			 * in this file
			 */
			for(glist = mt_imported_list;
			    glist != NULL;
			    glist = g_list_next(glist)
			)
			{
			    if(EDV_MIME_TYPE(glist->data) == mt)
				break;
			}
			if(glist != NULL)
			{
			    /* Already defined in this file, so set
			     * it NULL so that it is not defined again
			     */
			    g_printerr(
 "%s: Warning: Redefination of \"%s\" ignored.\n",
				path, type_str
			    );
			    mt = NULL;
			    mt_num = -1;
			}

			/* Check if this MIME Type is newer than the one
			 * on file and we are only want to import
			 * MIME Types that are newer on file
			 */
			if(only_newer && (mt != NULL))
			{
			    if(mt->modify_time >= file_last_modified)
			    {
				mt = NULL;
				mt_num = -1;
			    }
			}
		    }
		}
	    }

	    /* Insert or append? */
	    if((mt == NULL) && !mt_exists)
	    {
		/* Insert */
		if((cur_insert_index >= 0) && (cur_insert_index < *total))
		{
		    /* Allocate more pointers */
		    gint i = MAX(*total, 0);
		    *total = i + 1;
		    *list = (edv_mime_type_struct **)g_realloc(
		        *list,
		        (*total) * sizeof(edv_mime_type_struct *)
		    );
		    if(*list == NULL)
		    {
			g_printerr(
			    "%s: Memory allocation error.\n",
			    path
			);
		        *total = 0;
		        break;
		    }

		    mt_num = cur_insert_index;

		    /* Shift pointers */
		    for(i = (*total) - 1; i > mt_num; i--)
			(*list)[i] = (*list)[i - 1];

		    /* Create new MIME Type */
		    (*list)[mt_num] = mt = EDVMimeTypeNew(
		        EDV_MIME_TYPE_CLASS_FORMAT,
		        NULL,		/* Value */
		        type_str,	/* Type */
		        NULL		/* Description */
		    );

		    cur_insert_index++;	/* Account for the shift in position */
	        }
		else
		{
		    /* Append */

		    /* Allocate more pointers */
		    mt_num = MAX(*total, 0); 
		    *total = mt_num + 1;
		    *list = (edv_mime_type_struct **)g_realloc(
		        *list,
		        (*total) * sizeof(edv_mime_type_struct *)
		    );
		    if(*list == NULL)
		    {
			g_printerr(
			    "%s: Memory allocation error.\n",
			    path
			);
		        *total = 0;
		        break;
		    }

		    /* Create new MIME Type */
		    (*list)[mt_num] = mt = EDVMimeTypeNew(
		        EDV_MIME_TYPE_CLASS_FORMAT,
		        NULL,		/* Value */
		        type_str,	/* Type */
		        NULL		/* Description */
		    );

		    cur_insert_index = -1;
	        }
	    }

	    /* MIME Type valid for value setting? */
	    if((mt != NULL) && (mt_num >= 0) && (mt_num < *total))
	    {
		gchar **ext_list;
		edv_mime_type_struct *m = mt;

		/* Record this MIME Type as being imported from this
		 * file
		 */
		mt_imported_list = g_list_append(
		    mt_imported_list, m
		);

		/* Explode the extension list string */
		if(!STRISEMPTY(ext_str))
		    ext_list = (gchar **)g_strsplit(
			(const char *)ext_str, " ", -1
		    );
		else
		    ext_list = NULL;
		if(ext_list != NULL)
		{
		    gint i;
		    const gchar *ext;

		    g_free(m->value);
		    m->value = NULL;

		    /* Iterate through each string, converting them to
		     * a sequence of properly formatted extensions for
		     * the MIME Type value string
		     */
		    for(i = 0;
			ext_list[i] != NULL;
			g_free(ext_list[i]), i++
		    )
		    {
			ext = ext_list[i];
			if(STRISEMPTY(ext))
			    continue;

			/* Append this string to the value as an
			 * extension with a '.' prefix
			 *
			 * Add a space in front if this is not the
			 * first string
			 */
			m->value = G_STRCAT(
			    m->value,
			    (i > 0) ? " ." : "."
			);
			m->value = G_STRCAT(m->value, ext);
		    }

		    g_free(ext_list);
		}

		/* Update the time stamps */
		if(file_last_modified > m->modify_time)
		    m->modify_time = file_last_modified;
		if(file_last_modified > m->change_time)
		    m->change_time = file_last_modified;

		/* Report this MIME Type as added or modified */
		if(mt_exists)
		{
		    if(modified_cb != NULL)
			modified_cb(mt_num, m, modified_data);
		}
		else
		{
		    if(added_cb != NULL)
			added_cb(mt_num, m, added_data);
		}
	    }

#undef GET_NEXT_CHAR_LITERAL
	}

	/* Close the Media Types file */
	fclose(fp);

	/* Delete imported MIME Types list */
	g_list_free(mt_imported_list);

	/* Report the final progress */
	if((progress_cb != NULL) && !aborted)
	{
	    if(progress_cb(progress_data, file_size, file_size))
		aborted = TRUE;
	}
}

/*
 *	Exports the MIME Types to the Media Types file.
 *
 *	The path specifies the Media Types file.
 *
 *	The list and total specifies the MIME Types list.
 *
 *	If include_read_only is TRUE then MIME Types that are marked
 *	as read only will also be exported.
 *
 *	If progress_cb is not NULL then it will be called during the
 *	operation to report the progress. If it returns non-zero then
 *	the operation will be aborted.
 *
 *	The progress_data specifies the user data which will be passed
 *	to the progress_cb function.
 */
void EDVMimeTypesListMediaTypesExport(
	const gchar *path,
	edv_mime_type_struct **list, const gint total,
	const gboolean include_read_only,
	gint (*progress_cb)(gpointer, const gulong, const gulong),
	gpointer progress_data
)
{
	gboolean aborted = FALSE;
	gint mt_num;
	edv_mime_type_struct *mt;
	gchar *parent;
	FILE *fp;

	if((list == NULL) || STRISEMPTY(path))
	    return;

	/* Get parent directory and create it as needed */
	parent = g_dirname(path);
	if(parent != NULL)
	{
	    rmkdir((char *)parent, S_IRUSR | S_IWUSR | S_IXUSR);
	    g_free(parent);
	}

	/* Open the Media Types file for writing */
	fp = fopen((const char *)path, "wb");
	if(fp == NULL)
	    return;

	/* Report initial progress */
	if(progress_cb != NULL)
	{
	    if(progress_cb(progress_data, 0l, (gulong)total))
		aborted = TRUE;
	}


	/* Write header */
	fprintf(
	    fp,
"# Media Types\n\
#\n\
# Generated by %s Version %s\n\
#\n\
\n",
	    PROG_NAME_FULL, PROG_VERSION
	);


	/* Iterate through the MIME Type list */
	for(mt_num = 0; mt_num < total; mt_num++)
	{
	    if(aborted)
		break;

	    mt = list[mt_num];
	    if(mt == NULL)
		continue;

	    /* Skip MIME Types that are marked read only, meaning they
	     * should not be saved to file since they are created
	     * internally or are loaded from a global configuration
	     */
	    if(!include_read_only && mt->read_only)
		continue;

	    /* MIME Type string must be defined */
	    if(STRISEMPTY(mt->type))
		continue;

	    /* Report progress */
	    if(progress_cb != NULL)
	    {
		if(progress_cb(
		    progress_data,
		    (gulong)mt_num, (gulong)total
		))
		{
		    aborted = TRUE;
		    break;
		}
	    }


	    /* Begin writing Media Type line */

	    /* First value is the type value */
	    fputs((const char *)mt->type, fp);

	    /* If this MIME Type's class is set to file format then write
	     * subsequent values are extensions (without the '.' prefix)
	     */
	    if(mt->mt_class == EDV_MIME_TYPE_CLASS_FORMAT)
	    {
		const gchar *ext_str = mt->value;
		gchar **ext_list;

		/* Get the list of extensions from the value */
		if(ext_str != NULL)
		    ext_list = g_strsplit(ext_str, " ", -1);
		else
		    ext_list = NULL;
		if(ext_list != NULL)
		{
		    /* Iterate through the list of extensions */
		    gint i;
		    const gchar *ext;
		    for(i = 0;
			ext_list[i] != NULL;
			g_free(ext_list[i]), i++
		    )
		    {
			ext = ext_list[i];
			if(STRISEMPTY(ext))
			    continue;

			/* Write deliminator, a tab character if it is
			 * the first extension or space if it is a
			 * subsequent extension
			 */
 			fputc((i > 0) ? ' ' : '\t', fp);

			/* Write extension, seek past the first '.'
			 * deliminator so that it is not written
			 */
			while(*ext == '.')
			    ext++;
			fputs(ext, fp);
		    }
		    g_free(ext_list);
		}
	    }

	    /* Write line deliminator (newline character) */
	    fputc('\n', fp);
	}

	/* Close the Media Types file */
	fclose(fp);

	/* Report the final progress */
	if((progress_cb != NULL) && !aborted)
	{
	    if(progress_cb(progress_data, (gulong)total, (gulong)total))
		aborted = TRUE;
	}
}
