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

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

#include "edvtypes.h"
#include "edvmimetypes.h"
#include "edvmimetypesfio.h"
#include "config.h"


void EDVMimeTypeListMediaTypesImport(
	const gchar *filename,
	edv_mimetype_struct ***list, gint *total,
	gint insert_index,
	gboolean update, gboolean only_newer,
	gint (*progress_cb)(gpointer, gulong, gulong),
	gpointer progress_data,
	void (*added_cb)(gpointer, gint, edv_mimetype_struct *),
	gpointer added_data,
	void (*modified_cb)(gpointer, gint, edv_mimetype_struct *),
	gpointer modified_data
);
void EDVMimeTypeListMediaTypesExport(
	const gchar *filename,
	edv_mimetype_struct **list, gint total,
	gboolean include_read_only,
	gint (*progress_cb)(gpointer, gulong, 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 format file.
 *
 *	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.
 */
void EDVMimeTypeListMediaTypesImport(
	const gchar *filename,
	edv_mimetype_struct ***list, gint *total,
	gint insert_index,
	gboolean update, gboolean only_newer,
	gint (*progress_cb)(gpointer, gulong, gulong),
	gpointer progress_data,
	void (*added_cb)(gpointer, gint, edv_mimetype_struct *),
	gpointer added_data,
	void (*modified_cb)(gpointer, gint, edv_mimetype_struct *),
	gpointer modified_data
)
{
	gboolean aborted = FALSE, c_literal, mt_exists;
	gint i, c;
	FILE *fp;
	gulong	file_size = 0l,
		last_modified = 0l;
	gchar	*s,
		type_str[1024],
		ext_str[2048];
	GList *mt_imported_list = NULL;
	gint mt_num = -1;
	edv_mimetype_struct *mt_ptr = NULL;
	struct stat stat_buf;

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

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

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

	/* Report 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(TRUE)
	{
/* Gets the next character as c and updates c_literal */
#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;			\
 }					\
}
	    if(aborted)
		break;

	    /* 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? */
	    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 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_ptr = 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_ptr = EDVMimeTypeMatchListByType(
			*list, *total, &mt_num, type_str, FALSE
		    );
		    if(mt_ptr != 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_MIMETYPE(glist->data) == mt_ptr)
				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",
				filename, type_str
			    );
			    mt_ptr = 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_ptr != NULL))
			{
			    if(mt_ptr->modify_time >= last_modified)
			    {
				mt_ptr = NULL;
				mt_num = -1;
			    }
			}
		    }
		}
	    }

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

		    mt_num = 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_ptr = EDVMimeTypeNew(
		        EDV_MIMETYPE_CLASS_FORMAT,
		        NULL,		/* Value */
		        type_str,	/* Type */
		        NULL		/* Description */
		    );

		    insert_index++;
	        }
		else
		{
		    /* Append */

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

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

		    insert_index = -1;
	        }
	    }

	    /* MIME Type valid for value setting? */
	    if((mt_ptr != NULL) && (mt_num >= 0) && (mt_num < *total))
	    {
		gchar **strv;
		gint strc = 0;
		edv_mimetype_struct *m = mt_ptr;

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

		/* Explode extension string */
		if(!STRISEMPTY(ext_str))
		    strv = (gchar **)strexp(
			(const char *)ext_str, (int *)&strc
		    );
		else
		    strv = NULL;
		if(strv != NULL)
		{
		    gint n;

		    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(n = 0; n < strc; n++)
		    {
			s = strv[n];
			if(s == NULL)
			    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,
			    (n > 0) ? " ." : "."
			);
			m->value = G_STRCAT(m->value, s);

			g_free(strv[n]);
		    }

		    g_free(strv);
		}

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

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

#undef GET_NEXT_CHAR_LITERAL
	}

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

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

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

/*
 *	Exports the MIME Types to the Media Types format file.
 *
 *	If include_read_only is TRUE then MIME Types that are marked
 *	as read only will also be exported.
 */
void EDVMimeTypeListMediaTypesExport(
	const gchar *filename,
	edv_mimetype_struct **list, gint total,
	gboolean include_read_only,
	gint (*progress_cb)(gpointer, gulong, gulong),
	gpointer progress_data
)
{
	gboolean aborted = FALSE;
	gint mt_num;
	edv_mimetype_struct *mt_ptr;
	gchar *parent;
	FILE *fp;

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

	/* Get parent directory and create it as needed */
	parent = g_dirname(filename);
	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 *)filename, "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",
	    PROG_NAME_FULL, PROG_VERSION
	);


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

	    mt_ptr = list[mt_num];
	    if(mt_ptr == 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_ptr->read_only)
		continue;

	    /* MIME Type string must be defined */
	    if(STRISEMPTY(mt_ptr->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_ptr->type, fp);

	    /* If this MIME Type's class is set to file format then write
	     * subsequent values are extensions (without the '.' prefix)
	     */
	    if(mt_ptr->mt_class == EDV_MIMETYPE_CLASS_FORMAT)
	    {
		gchar **strv;
		gint n, strc;

		/* Explode value string which should contain a space
		 * separated list of extensions
		 */
		strv = strexp(mt_ptr->value, &strc);
		if(strv != NULL)
		{
		    /* Iterate throughe exploded extension strings */
		    for(n = 0; n < strc; n++)
		    {
			const gchar *cs = strv[n];
			if(cs == NULL)
			    continue;

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

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

			/* Delete exploded string */
			g_free(strv[n]);
			strv[n] = NULL;
		    }

		    /* Delete pointer array only */
		    g_free(strv);
		    strv = NULL;
		    strc = 0;
		}
	    }

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

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

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