#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <glib.h>
#include <unistd.h>

#include "cfg.h"
#include "edv_interps.h"
#include "edv_utils.h"
#include "edv_cfg_list.h"
#include "config.h"


static gchar *EDVInterPSGetLockPath(const cfg_item_struct *cfg_list);
static gchar *EDVInterPSGetCmdPath(const cfg_item_struct *cfg_list);

/* InterPS Lock Link */
gint EDVInterPSGetLock(const cfg_item_struct *cfg_list);
gint EDVInterPSMakeLock(
	const cfg_item_struct *cfg_list,
	const gint p, const gboolean force
);
void EDVInterPSRemoveLock(const cfg_item_struct *cfg_list);

/* InterPS Command */
gboolean EDVInterPSHaveCommand(const cfg_item_struct *cfg_list);
void EDVInterPSSendCommandsList(
	const cfg_item_struct *cfg_list,
	const gint p,
	gchar **cmd_list
);
gchar **EDVInterPSGetCommandsList(const cfg_item_struct *cfg_list);
void EDVInterPSRemoveCommandsList(const cfg_item_struct *cfg_list);


#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 dynamically allocated string describing the full
 *	path to the InterPS lock link
 */
static gchar *EDVInterPSGetLockPath(const cfg_item_struct *cfg_list)
{
	const gchar *env_lock_link = g_getenv(ENV_VAR_NAME_EDVINTERPSLOCK);
	if(env_lock_link != NULL)
	{
	    return(STRDUP(env_lock_link));
	}
	else
	{
	    const gchar *local_data_dir = CFGItemListGetValueS(
		cfg_list, EDV_CFG_PARM_DIR_LOCAL
	    );
	    return(g_strconcat(
		(local_data_dir != NULL) ? local_data_dir : "",
		G_DIR_SEPARATOR_S,
		EDV_INTERPS_LOCK_LINK,
		NULL
	    ));
	}
}

/*
 *	Returns a dynamically allocated string describing the full
 *	path to the InterPS command file
 */
static gchar *EDVInterPSGetCmdPath(const cfg_item_struct *cfg_list)
{
	const gchar *env_cmd_file = g_getenv(ENV_VAR_NAME_EDVINTERPSCMDFILE);
	if(env_cmd_file != NULL)
	{
	    return(STRDUP(env_cmd_file));
	}
	else
	{   
	    const gchar *local_data_dir = CFGItemListGetValueS(
		cfg_list, EDV_CFG_PARM_DIR_LOCAL
	    );
	    return(g_strconcat(
		(local_data_dir != NULL) ? local_data_dir : "",
		G_DIR_SEPARATOR_S,
		EDV_INTERPS_CMD_FILE,
		NULL
	    ));
	}
}


/*
 *	Returns the pid of the currently running process of Endeavour or
 *	0 if it is not running/error.
 *
 *	The lock link will be checked and the actual process will be
 *	checked to see if it is actually running.
 */
gint EDVInterPSGetLock(const cfg_item_struct *cfg_list)
{
	gint p;
	gchar *v;
	gchar *lock_link = EDVInterPSGetLockPath(cfg_list);
	if(lock_link == NULL)
	    return(0);

	/* Get the lock link's value */
	v = EDVGetLinkValue(lock_link);
	if(v == NULL)
	{
	    /* Lock link does not exist or error reading it */
	    g_free(lock_link);
	    return(0);
	}

	g_free(lock_link);	/* Don't need this anymore */

	/* Get pid from the link's destination and check if it is
	 * actually running
	 */
	p = ATOI(v);
	g_free(v);
	if(EDVProcessIsRunning(p))
	    return(p);
	else
	    return(0);
}

/*
 *	Creates a new InterPS lock link that refers to the specified
 *	pid.
 *
 *	The link will be tested to see if it already exists. If it does
 *	not exist or force is TRUE then a new lock will be created and
 *	refer to the specified pid.
 *
 *	Returns:
 *
 *	0	Success or link already exists.
 *	-1	General error or unable to (re)create link.
 *	-2	Invalid value.
 *	-3	Systems error or memory allocation error.
 */
gint EDVInterPSMakeLock(
	const cfg_item_struct *cfg_list,
	const gint p, const gboolean force
)
{
	gchar num_str[40];
	gchar *lock_link = EDVInterPSGetLockPath(cfg_list);
	if(lock_link == NULL)
	    return(-2);

	/* Lock link already exists and not forcing? */
	if(!access((const char *)lock_link, F_OK) && !force)
	{
	    g_free(lock_link);
	    return(0);
	}

	/* Format the link destination, which is the pid of the
	 * specified process p
	 */
	g_snprintf(
	    num_str, sizeof(num_str),
	    "%i",
	    p
	);

	/* (Re)create the lock link */
	if(unlink((const char *)lock_link))
	{
	    const gint error_code = errno;
	    if(error_code != ENOENT)
	    {
		/* The object exists but was not able to be removed */
		g_free(lock_link);
		return(-1);
	    }
	}
	if(symlink((const char *)num_str, (const char *)lock_link))
	{
	    g_free(lock_link);
	    return(-1);
	}

	g_free(lock_link);

	return(0);
}

/*
 *	Removes the InterPS lock link.
 */
void EDVInterPSRemoveLock(const cfg_item_struct *cfg_list)
{
	gchar *lock_link = EDVInterPSGetLockPath(cfg_list);
	if(lock_link != NULL)
	{
	    unlink((const char *)lock_link);
	    g_free(lock_link);
	}
}


/*
 *	Checks if the InterPS command file exists, which also checks
 *	if there is a pending InterPS command.
 */
gboolean EDVInterPSHaveCommand(const cfg_item_struct *cfg_list)
{
	gint status;
	gchar *cmd_file = EDVInterPSGetCmdPath(cfg_list);
	if(cmd_file == NULL)
	    return(FALSE);

	/* Command file exists? */
	status = (gint)access((const char *)cmd_file, F_OK);

	g_free(cmd_file);

	return((status == 0) ? TRUE : FALSE);
}

/*
 *	Sends the InterPS commands list to the process.
 *
 *	The p specifies the process ID.
 *
 *	The cmd_list specifies the command list. The last pointer in
 *	the array must be NULL to mark the end of the list.
 *
 *	A newline character is automatically be appended to each
 *	InterPS command as it is written to the InterPS command file.
 *	Once the InterPS command file has been appended with the
 *	commands list, a SIGUSR1 is sent ot the process notifying it
 *	to process the commands.
 */
void EDVInterPSSendCommandsList(
	const cfg_item_struct *cfg_list,
	const gint p,
	gchar **cmd_list
)
{
	gint i;
	gchar *cmd_file;
	FILE *fp;

	if((p <= 0) || (cmd_list == NULL))
	    return;

	cmd_file = EDVInterPSGetCmdPath(cfg_list);
	if(cmd_file == NULL)
	    return;

	/* Open the command file for append writing */
	fp = fopen((const char *)cmd_file, "ab");
	if(fp != NULL)
	{
            /* Set the command file's permissions so that only
             * the owner can read and write to it
             */
            if(fchmod(fileno(fp), S_IRUSR | S_IWUSR))
	    {
		fclose(fp);
		g_free(cmd_file);
                return;
            }

	    /* Append the commands to the command file */
	    for(i = 0; cmd_list[i] != NULL; i++)
		fprintf(
		    fp,
		    "%s\n",
		    (const char *)cmd_list[i]
		);

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

#ifdef SIGUSR1
	    /* Notify the running Endeavour process */
	    if(p > 0)
		kill((int)p, SIGUSR1);
#else
#warning SIGUSR1 is not defined, InterPS command support will not be fully compiled and functional
#endif
	}

	g_free(cmd_file);
}

/*
 *	Gets all the InterPS commands from the InterPS command file.
 *
 *	Returns a dynamically allocated list of strings describing
 *	the InterPS commands list, the last pointer in the list will be
 *	NULL to mark the end of the list.
 */
gchar **EDVInterPSGetCommandsList(const cfg_item_struct *cfg_list)
{
	struct stat stat_buf;
	FILE *fp;
	gchar *buf, *read_buf, **cmd_list;
	gint i, buf_len, units_read;
	gulong read_buf_len;
	gchar *cmd_file = EDVInterPSGetCmdPath(cfg_list);
	if(cmd_file == NULL)
	    return(NULL);

	/* Open the command file for reading */
	fp = fopen((const char *)cmd_file, "rb");
	g_free(cmd_file);
	if(fp == NULL)
	    return(NULL);

	/* Get the command file's statistics */
	if(fstat(fileno(fp), &stat_buf))
	{
	    fclose(fp);
	    return(NULL);
	}

	/* Skip if the command file is empty */
	if(stat_buf.st_size == 0l)
	{
	    fclose(fp);
	    return(NULL);
	}

	/* Allocate the read buffer */
	read_buf_len = (gulong)(stat_buf.st_blksize / sizeof(gchar));
	if(read_buf_len == 0l)
	    read_buf_len = 1024l;
	read_buf = (gchar *)g_malloc(read_buf_len);
	if(read_buf == NULL)
	{
	    fclose(fp);
	    return(NULL);
	}

	/* Begin reading the file to the buffer */
	buf = NULL;
	buf_len = 0;
	while(!feof(fp))
	{
	    /* Read the next block */
	    units_read = (gint)fread(
		read_buf, sizeof(gchar), (size_t)read_buf_len, fp
	    );
	    if(units_read <= 0)
		break;

	    /* Reallocate the buffer and append this block to it */
	    i = buf_len;
	    buf_len += units_read * sizeof(gchar);
	    buf = (gchar *)g_realloc(buf, buf_len + 1);
	    if(buf == NULL)
	    {
		buf_len = 0;
		break;
	    }
	    memcpy(buf + i, read_buf, units_read * sizeof(gchar));
	}

	/* Delete the read buffer and close the file */
	g_free(read_buf);
	fclose(fp);

	/* Was any data read from the command file and we have a buffer? */
	if((buf != NULL) && (buf_len > 0))
	{
	    /* Null terminate the buffer */
	    gchar *buf_ptr = buf + buf_len;
	    *buf_ptr = '\0';

	    /* Remove the last newline deliminator as needed */
	    if(buf_ptr > buf)
	    {
		buf_ptr--;
		if(*buf_ptr == '\n')
		    *buf_ptr = '\0';
	    }
	}

	/* Explode the commands at all newline deliminators */
	cmd_list = g_strsplit(buf, "\n", -1);

	/* Delete the buffer */
	g_free(buf);

	return(cmd_list);
}

/*
 *	Removes the InterPS command file.
 */
void EDVInterPSRemoveCommandsList(const cfg_item_struct *cfg_list)
{
	gchar *cmd_file = EDVInterPSGetCmdPath(cfg_list);
	if(cmd_file != NULL)
	{
	    unlink((const char *)cmd_file);
	    g_free(cmd_file);
	}
}
