#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <gtk/gtk.h>
#include <unistd.h>

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

#include "guiutils.h"
#include "cdialog.h"

#include "viewer.h"
#include "viewerfio.h"

#include "manedit.h"
#include "maneditop.h"
#include "config.h"


static gint ViewerOpenProgressCB(
	const gulong i, const gulong n,
	gpointer data
);
gint ViewerOpenFile(
	viewer_struct *v, const gchar *filename, const gchar *name
);


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


/*
 *	Viewer load progress callback.
 */
static gint ViewerOpenProgressCB(
	const gulong i, const gulong n,
	gpointer data
)
{
	gfloat percent;
	viewer_struct *v = VIEWER(data);
	if(v == NULL)
	    return(0);

	if(n > 0l)
	    percent = (gfloat)i / (gfloat)n;
	else
	    percent = 1.0f;

	ViewerSetStatusProgress(v, percent);

	return(0);
}


/*
 *	Opens the manual page.
 *
 *	The filename specifies the manual page.
 *
 *	The name specifies the name of the manual page.
 */
gint ViewerOpenFile(
	viewer_struct *v, const gchar *filename, const gchar *name
)
{
	FILE *fp;
	struct stat stat_buf;
	gint status, fd, buf_len;
	gchar	*buf,
		*prog_tmp_dir,
		*stdout_path,
		*stderr_path;
	const gchar *converter_cmd;
	GtkAdjustment *vadj;
	GtkWidget *toplevel;
	medit_core_struct *core;

	if((v == NULL) || (filename == NULL))
	    return(-2);

	toplevel = v->toplevel;
	core = v->core;

	/* If given filename is an absolute path, then check if it
	 * exists. This is to allow the converter program to search for
	 * relative paths
	 */
	if(ISPATHABSOLUTE(filename))
	{
	    if(stat((const char *)filename, &stat_buf))
		return(-1);
	}

	/* Get the manual page to output converter command */
	converter_cmd = (const gchar *)PrefParmGetValueP(
	    core->pref,
	    MEDIT_PREF_PARM_CONVERTERS_MANPAGE_TO_OUTPUT
	);
	if((converter_cmd != NULL) ? ((*converter_cmd) == '\0') : TRUE)
	{
	    CDialogSetTransientFor(toplevel);
	    CDialogGetResponse(
		"Open Manual Page Failed",
"Manual page to output converter command was not\n\
defined.",
"The command to convert manual pages to readable\n\
output was not been defined, to define this\n\
command, go to Edit->Preferences->Converters.",
		CDIALOG_ICON_WARNING,
		CDIALOG_BTNFLAG_OK | CDIALOG_BTNFLAG_HELP,
		CDIALOG_BTNFLAG_OK
	    );
	    CDialogSetTransientFor(NULL);
	    return(-1);
	}

	/* Create tempory files directory for this program as needed
	 * (return string needs to be deleted).
	 */
	prog_tmp_dir = MEditCreateTempDir(core);
	if(prog_tmp_dir == NULL)
	{
	    CDialogSetTransientFor(toplevel);
	    CDialogGetResponse(
		"Open Manual Page Failed",
		"Unable to create temporary directory.",
		NULL,
		CDIALOG_ICON_ERROR,
		CDIALOG_BTNFLAG_OK,
		CDIALOG_BTNFLAG_OK
	    );
	    CDialogSetTransientFor(NULL);
	    return(-1);
	}

	/* Get standard output file path to output conversion to,
	 * note that stdout_path needs to be deleted later
	 */
	stdout_path = STRDUP(PrefixPaths(
	    prog_tmp_dir, "preview_stdoutXXXXXX"
	));
	fd = (gint)mkstemp((char *)stdout_path);
	if(fd > -1)
	    close((int)fd);

	stderr_path = STRDUP(PrefixPaths(
	    prog_tmp_dir, "preview_stderrXXXXXX"
	));
	fd = (gint)mkstemp((char *)stderr_path);
	if(fd > -1)
	    close((int)fd);

	/* Delete the temprary directory path */
	g_free(prog_tmp_dir);
	prog_tmp_dir = NULL;

	/* No output file path available? */
	if(stdout_path == NULL)
	{
	    g_free(stdout_path);
	    g_free(stderr_path);
	    return(-1);
	}

	/* Begin converting */

	v->freeze_count++;
	ViewerSetBusy(v);
	ViewerSetStatusProgress(v, 0.0f);
	ViewerSetStatusMessage(v, "Converting...");

	/* Convert file specified to groff output as a tempory
	 * file specified by stdout_path
	 */

/* Define values in enviroment variables? */

	status = MEditExecuteFmtBlock(
	    core,
	    converter_cmd,	/* Command */
	    filename,		/* Filename */
	    NULL,		/* Options */
	    stdout_path,	/* Stdout path */
	    stderr_path		/* Stderr path */
	);

	ViewerSetStatusMessage(v, "Convert done");

	/* Report any errors during the conversion */
	MEditDialogFromFile(core, stderr_path, toplevel);

	/* Error occured during execution or the converted file does
	 * not exist?
	 */
	if((status != 0) ||
	   stat((const char *)stdout_path, &stat_buf)
	)
	{
	    unlink(stdout_path);
	    g_free(stdout_path);
	    unlink(stderr_path);
	    g_free(stderr_path);
	    ViewerSetStatusProgress(v, 0.0f);
	    ViewerSetStatusMessage(v, "Open done");
	    ViewerSetReady(v);
	    v->freeze_count--;
	    return(-2);
	}

	ViewerSetStatusMessage(v, "Opening...");

	/* Allocate the buffer to open the file's contents */
	buf_len = (gint)stat_buf.st_size;
	if(buf_len < 0)
	    buf_len = 0;
	buf = (gchar *)g_malloc((buf_len + 1) * sizeof(gchar));
	if(buf == NULL)
	{
	    unlink(stdout_path);
	    g_free(stdout_path);
	    unlink(stderr_path);
	    g_free(stderr_path);
	    ViewerSetStatusProgress(v, 0.0f);
	    ViewerSetStatusMessage(v, "Open done");
	    ViewerSetReady(v);
	    v->freeze_count--;
	    return(-3);
	}

	/* Open the file for reading */
	fp = fopen((const char *)stdout_path, "rb");
	if(fp == NULL)
	{
	    unlink(stdout_path);
	    g_free(stdout_path);
	    unlink(stderr_path);
	    g_free(stderr_path);
	    ViewerSetStatusProgress(v, 0.0f);
	    ViewerSetStatusMessage(v, "Open done");
	    ViewerSetReady(v);

	    g_free(buf);

	    v->freeze_count--;
	    return(-2);
	}

	/* Begin opening the converted file */
	if(stat_buf.st_blksize > 0l)
	{
	    gint units_read;
	    const gulong read_buf_len = (gulong)stat_buf.st_blksize;
	    gchar	*buf_ptr = buf,
			*buf_end = buf_ptr + buf_len;

	    while(buf_ptr < buf_end)
	    {
		units_read = (gint)fread(
		    buf_ptr,
		    sizeof(gchar),
		    MIN((buf_end - buf_ptr), read_buf_len),
		    fp
		);
		if(units_read <= 0)
		{
		    *buf_ptr = '\0';
		    break;
		}

		buf_ptr += units_read;

		if(buf_len > 0)
		    ViewerSetStatusProgress(
			v,
			(gfloat)(buf_ptr - buf) / (gfloat)buf_len
		    );
	    }
	    buf[buf_len] = '\0';
	}
	else
	{
	    const gint units_read = (gint)fread(
		buf,
		sizeof(gchar),
		buf_len,
		fp
	    );
	    if(units_read < buf_len)
	    {
		if(units_read > 0)
		    buf[units_read] = '\0';
		else
		    *buf = '\0';
	    }
	    else
	    {
		buf[buf_len] = '\0';
	    }

	    ViewerSetStatusProgress(v, 1.0f);
	}

	/* Close the file */
	fclose(fp);
	fp = NULL;

	/* Remove tempory output file and delete output path */
	unlink(stdout_path);
	g_free(stdout_path);
	stdout_path = NULL;

	unlink(stderr_path);
	g_free(stderr_path);
	stderr_path = NULL;

	/* Set the buffer to the viewer */
	ViewerTextInsert(
	    v, buf, buf_len,
	    ViewerOpenProgressCB, v
	);

	ViewerSetStatusProgress(v, 1.0f);

	/* Delete the buffer */
	g_free(buf);
	buf = NULL;
	buf_len = 0;

	v->freeze_count--;


	/* Check if the last manual page path matches the new one */
	if((v->cur_manpage_name != NULL) && (name != NULL))
	{
	    /* Name same? */
	    if((v->cur_manpage_name == name) ?
	        TRUE : !strcmp(v->cur_manpage_name, name)
	    )
	    {
		/* Names match, so scroll to last view_text position */
		GtkText *text = GTK_TEXT(v->view_text);
		vadj = text->vadj;
		if(vadj != NULL)
		{
		    if(v->last_scroll_vpos < vadj->lower)
			v->last_scroll_vpos = vadj->lower;
		    if(v->last_scroll_vpos > (vadj->upper + vadj->page_size))
			v->last_scroll_vpos = vadj->upper + vadj->page_size;
		    gtk_adjustment_set_value(
			vadj, v->last_scroll_vpos
		    );
		}
	    }
	}


	/* Record the current opened manual page file on viewer */
	if((filename != v->cur_manpage_path) && (filename != NULL))
	{
	    g_free(v->cur_manpage_path);
	    v->cur_manpage_path = STRDUP(filename);
	}

	/* Record the name */
	if((name != v->cur_manpage_name) && (name != NULL))
	{
	    g_free(v->cur_manpage_name);
	    v->cur_manpage_name = STRDUP(name);
	}


	/* Update the last opened path on the viewer */
	g_free(v->last_open_path);
	v->last_open_path = STRDUP(filename);
	if(v->last_open_path != NULL)
	{
	    gchar *s = (gchar *)strrchr(
		(char *)v->last_open_path, G_DIR_SEPARATOR
	    );
	    if(s != NULL)
		*s = '\0';
	}


	ViewerSetStatusProgress(v, 0.0f);
	ViewerSetStatusMessage(v, "Open done");

	ViewerUpdateMenus(v);
	ViewerSetReady(v);

	/* Switch to viewer page */
	if(v->main_notebook != NULL)
	{
	    gtk_notebook_set_page(
		GTK_NOTEBOOK(v->main_notebook), ViewerPageNumView
	    );
	}

	/* Update text on current manpage combo */
	if(v->manpage_combo != NULL)
	{
	    GtkCombo *combo = GTK_COMBO(v->manpage_combo);
	    const gchar *s = (const gchar *)strrchr(
		(const char *)filename, ' '
	    );
	    if(s != NULL)
	    {
		s++;
	        GUIComboAddItem(GTK_WIDGET(combo), s);
	        gtk_entry_set_text(GTK_ENTRY(combo->entry), s);
	    }
	    else
	    {
		GUIComboAddItem(GTK_WIDGET(combo), filename);
		gtk_entry_set_text(GTK_ENTRY(combo->entry), filename);
	    }
	    gtk_editable_select_region(GTK_EDITABLE(combo->entry), 0, -1);

	    /* Also, if manpage_combo specifies a full path, then select
	     * the exact section on the section combo
	     */
	    if(v->section_pulistbox != NULL)
	    {
		GtkEntry *manpage_entry = GTK_ENTRY(combo->entry);

		s = gtk_entry_get_text(manpage_entry);
		if((s != NULL) ? (*s == G_DIR_SEPARATOR) : FALSE)
		{
		    gint i, n;
		    const gchar *s;
		    pulist_struct *pulist = PUListBoxGetPUList(
			v->section_pulistbox
		    );

		    n = PUListGetTotalItems(pulist);
		    for(i = 0; i < n; i++)
		    {
			s = (const gchar *)PUListGetItemData(pulist, i);
			if(s == NULL)
			    continue;

			if(!g_strcasecmp(s, MEDIT_SECT_NAME_EXACT))
			{
			    PUListUnselectAll(pulist);
			    PUListSelect(pulist, i);
			    break;
			}
		    }
		}
	    }
	}

	return(0);
}
