/*
 * logfile.c: logapp logfile handling
 *
 *
 * Copyright (C) 2007-2009 Michael Brunner <mibru@gmx.de>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * General Public License for more details.
 */

#include "logapp.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <pthread.h>

#include "configuration.h"
#include "logfile.h"

static int logfile_check_size(logfile_t* logfile, int addmarker,
			      unsigned addsize);
static int logfile_rename(logfile_t* logfile, char* oldname);
static char* logfile_get_alternate_name(logfile_t* logfile);
static int logfile_add_header(logfile_t* logfile);
static int logfile_add_splitmarker(logfile_t* logfile);
static int logfile_add_footer(logfile_t* logfile);

pthread_mutex_t logfile_mutex = PTHREAD_MUTEX_INITIALIZER;

extern char **environ;

#define MAX_HEADER	80
#define MAX_FOOTER	MAX_HEADER
#define MAX_SPLIT	MAX_HEADER
#define MAX_PWD		500	
#define MAX_TMP		50
static int logfile_add_header(logfile_t* logfile)
{
	int ret;
	int len;
	int headlen;
	int i;
	struct tm* loctime;
	char buf[MAX_HEADER + 1];
	char tmp[MAX_TMP + 1];
	char pwd[MAX_PWD + 1];

	/* Get to the end of the logfile and check size */
	if (lseek(logfile->fh, 0, SEEK_END) != 0) {
		if (write(logfile->fh, "\n", 1) == -1)
			return -1;

		if (logfile_check_size(logfile, 0, MAX_HEADER * 2)) {
			error("Problem while checking/resizing logfile\n");
			return -1;
		}
	}

	buf[MAX_HEADER] = '\0';

	headlen = strlen(logfile->head);

	/* Write application name */
	len = snprintf(buf, MAX_HEADER, "%s %s\n", logfile->head, app.exe);
	if ((len < 0) || (len > MAX_HEADER)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write argument list */
	for (i=1; i<app.argc; i++) {
		if (write(logfile->fh, logfile->head, headlen) == -1)
			return -1;
		len = snprintf(buf, MAX_HEADER, " arg[%d] = \"", i);
		if ((len < 0) || (len > MAX_HEADER)) {
			return -1;
		}
		if (write(logfile->fh, buf, len) == -1)
			return -1;
		if (write(logfile->fh, app.argv[i], strlen(app.argv[i])) == -1)
			return -1;
		if (write(logfile->fh, "\"", 1) != 1)
			return -1;
		if ((i+1)<app.argc)
			if (write(logfile->fh, "\n", 1) != 1)
				return -1;
	}
	if (app.argc<2) {
		if (write(logfile->fh, logfile->head, headlen) == -1)
			return -1;
		if (write(logfile->fh, " <no command line options>", 26)
		    != 26 )
			return -1;
	}
	if (write(logfile->fh, "\n", 1) != 1)
		return -1;

	/* Write current path */
	len = snprintf(buf, MAX_HEADER, "%s pwd = ", logfile->head);
	if ((len < 0) || (len > MAX_HEADER)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;
	if (getcwd(pwd, MAX_PWD) == NULL) {
		if (write(logfile->fh, "<unable to get working directory>\n",
			  34) != 34)
			return -1;
	} else {
		pwd[strlen(pwd)+1] = '\0';
		pwd[strlen(pwd)] = '\n';
		if (write(logfile->fh, pwd, strlen(pwd)) == -1)
			return -1;
	}

	/* Write environment variables */
	if (config.logenv) {
		for (i=0; environ[i] != NULL; i++) {
			if (write(logfile->fh, logfile->head, headlen) == -1)
				return -1;
			len = snprintf(buf, MAX_HEADER, " env[%d] = \"", i);
			if ((len < 0) || (len > MAX_HEADER)) {
				return -1;
			}
			if (write(logfile->fh, buf, len) == -1)
				return -1;
			if (write(logfile->fh, environ[i], strlen(environ[i]))
			    == -1)
				return -1;
			if (write(logfile->fh, "\"\n", 2) != 2)
				return -1;
		}
	}

	/* Write starttime */
	loctime = localtime(&app.starttime);
	ret = strftime(tmp, MAX_TMP, "%Y-%m-%d %T %Z", loctime);
	
	if ((app.starttime < 0) || (ret == 0)) {
		tmp[0] = '\0';
	}
	snprintf(buf, MAX_HEADER, "%s %s\n", logfile->head, tmp);
	if ((len < 0) || (len > MAX_HEADER)) {
		return -1;
	}
	if (write(logfile->fh, buf, strlen(buf)) == -1)
		return -1;

	return 0;
}

static int logfile_add_splitmarker(logfile_t* logfile)
{
	int ret;
	int len;
	time_t sec;
	struct tm* loctime;
	char buf[MAX_SPLIT + 1];
	char tmp[MAX_TMP + 1];

	buf[MAX_SPLIT] = '\0';

	/* Write application name */
	len = snprintf(buf, MAX_SPLIT, "%s %s\n", logfile->split, app.exe);
	if ((len < 0) || (len > MAX_SPLIT)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write starttime */
	loctime = localtime(&app.starttime);
	ret = strftime(tmp, MAX_TMP, "%Y-%m-%d %T %Z", loctime);
	
	if ((app.starttime < 0) || (ret == 0)) {
		tmp[0] = '\0';
	}
	len = snprintf(buf, MAX_SPLIT, "%s running since %s\n", logfile->split,
		       tmp);
	if ((len < 0) || (len > MAX_SPLIT)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write current line count */
	len = snprintf(buf, MAX_SPLIT, "%s %d lines output (%d stdout, %d"
		       " stderr)\n", logfile->split,
		 app.pstdout-> linecount + app.pstderr->linecount,
		 app.pstdout->linecount, app.pstderr->linecount);
	if ((len < 0) || (len > MAX_SPLIT)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write execution time */
	if (time(&sec) > -1) {
		loctime = localtime(&sec);
		ret = strftime(tmp, MAX_TMP, "%Y-%m-%d %T %Z", loctime);
	} 
	
	if ((sec < 0) || (ret == 0)) {
		tmp[0] = '\0';
	}

	if ((app.starttime > -1)&&(app.starttime <= sec)) {
		len = snprintf(buf, MAX_SPLIT, "%s execution time is %us\n",
			 logfile->split, (unsigned int)(sec - app.starttime));
		if ((len < 0) || (len > MAX_SPLIT)) {
			return -1;
		}
		if (write(logfile->fh, buf, len) == -1)
			return -1;
	} else {
		error("Problem calculating execution time\n");
	}

	return 0;
}

static int logfile_add_footer(logfile_t* logfile)
{
	int len = 0;
	int tmplen = 0;
	time_t sec;
	struct tm* loctime;
	char buf[MAX_FOOTER + 1];
	char tmp[MAX_TMP + 1];

	/* Add a newline if needed */
	if (logfile->addnewline)
		if (write(logfile->fh, "\n", 1) == -1)
			return -1;

	buf[MAX_FOOTER] = '\0';

	/* Write exit time */
	if (time(&sec) > -1) {
		loctime = localtime(&sec);
		len = strftime(tmp, MAX_TMP, "%Y-%m-%d %T %Z", loctime);
	} 
	
	if ((sec < 0) || (len < 1)) {
		tmp[0] = '\0';
		len = 0;
	}

	snprintf(buf, MAX_FOOTER, "%s %s\n", logfile->foot, tmp);
	if ((len < 0) || (len > MAX_FOOTER)) {
		return -1;
	}
	len += strlen(logfile->foot) + 2;
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write exit status */
	len = snprintf(buf, MAX_FOOTER, "%s application exit status: ",
		 logfile->foot);
	if ((len < 0) || (len > MAX_FOOTER)) {
		return -1;
	}
	if (WIFEXITED(app.exit_state)) {
		tmplen = snprintf(buf + len, MAX_FOOTER - len, "%d\n",
			 WEXITSTATUS(app.exit_state));
	} else if (WIFSIGNALED(app.exit_state)) {
		tmplen = snprintf(buf + len, MAX_FOOTER - len,
			 "abnormal termination (signal = %d)\n",
			 WTERMSIG(app.exit_state));
	} else {
		tmplen += snprintf(buf + len, MAX_FOOTER - len, "unknown\n");
	}
	if ((tmplen < 0) || (tmplen > MAX_FOOTER)) {
		return -1;
	}
	len += tmplen;

	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write line counts */
	len = snprintf(buf, MAX_FOOTER, "%s %d lines output (%d stdout, %d"
		       " stderr)\n", logfile->foot,
		 app.pstdout-> linecount + app.pstderr->linecount,
		 app.pstdout->linecount, app.pstderr->linecount);
	if ((len < 0) || (len > MAX_FOOTER)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	/* Write execution time */
	if ((app.starttime > -1)&&(app.starttime <= sec)) {
		len = snprintf(buf, MAX_FOOTER, "%s execution time was %us\n",
			 logfile->foot, (unsigned int)(sec - app.starttime));
		if ((len < 0) || (len > MAX_FOOTER)) {
			return -1;
		}
		if (write(logfile->fh, buf, len) == -1)
			return -1;
	} else {
		error("problem calculating execution time\n");
	}

	/* Write application name */
	len = snprintf(buf, MAX_FOOTER, "%s %s\n", logfile->foot, app.exe);
	if ((len < 0) || (len > MAX_FOOTER)) {
		return -1;
	}
	if (write(logfile->fh, buf, len) == -1)
		return -1;

	return 0;
}

static int logfile_rename(logfile_t* logfile, char* oldname)
{
	char* newname;
	int ret = 0;

	if (!config.logrename)
		return 0;

	if (oldname == NULL)
		oldname = logfile->name;

	newname = malloc(strlen(oldname) 
			 + sizeof(logfile->oldext));
	if (newname == NULL) {
		error_outofmemory();
		return -1;
	}

	strcpy(newname, oldname);
	strcat(newname, logfile->oldext);

	if (rename(oldname, newname)) {
		if (errno != ENOENT) {
			ret = -1;
			warning("logfile rename failed (error: %d)\n", errno);
		}
	}

	free(newname);

	return ret;
}

static char* logfile_get_alternate_name(logfile_t* logfile)
{
	int len;

	if (!logfile->appendnr) {
		return strdup(logfile->name);
	}

	char number[12];

	len = snprintf(number, 12, ".%d", logfile->appendnr);
	if (len < 0) {
		return NULL;
	}
	
	char *name = malloc(strlen(logfile->name) + strlen(number) + 1);
	if (name == NULL) {
		error_outofmemory();
		return NULL;
	}

	strcpy(name, logfile->name);
	strcat(name, number);

	return name;
}

static int logfile_check_size(logfile_t* logfile, int addmarker,
			      unsigned addsize)
{
	unsigned curpos;

	if (!logfile->sizelimit)
		return 0;

	curpos = lseek(logfile->fh, 0, SEEK_CUR) + addsize;

	if (curpos > logfile->sizelimit) {
		int  flags = O_CREAT | O_WRONLY;

		if (addmarker)
			logfile_add_splitmarker(logfile);

		if (!config.circularlog) {
			if (logfile_rename(logfile, NULL))
				error("problem moving log - continuing "
				      "anyways\n");

			flags |= O_TRUNC;
		} else {
			if (ftruncate(logfile->fh,
				  lseek(logfile->fh, 0, SEEK_CUR)) == -1)
				error("Problem truncating logfile (errno=%d)",
				      errno);
			return -1;
		}

		close(logfile->fh);
		logfile->fh = open(logfile->name, flags, 0644);

		if (addmarker)
			logfile_add_splitmarker(logfile);
	}

	return 0;
}

int logfile_write(logfile_t* logfile, pipe_t* pipe)
{
	unsigned stamp_sec = 0;
	int stamp_msec = 0;
	int writetimestamp = config.logtime;
	int prefixlen = 0;

	if (!logfile->name)
		return 0;

	if (CONFIG_USE_THREADS)
		pthread_mutex_lock(&logfile_mutex);

	if (logfile->sizelimit)
		if (logfile_check_size(logfile, 1, pipe->bfill)) {
			error("Problem while checking/resizing logfile\n");
			return -1;
		}

	/* Get times for join and timestamp */
	if (config.logtime || (config.jointimeout && pipe->charbased)) {
		struct timeval tvtime;

		gettimeofday(&tvtime, NULL);
		if (config.logtimerstregexp) {
			if (!regexec(&config.plogtimerstreg, pipe->buf,
				     pipe->bfill, NULL, 0)) {
				memcpy(&app.toffset, &tvtime,
				       sizeof(app.toffset));
			}
		}

		stamp_sec = (unsigned) (tvtime.tv_sec - app.toffset.tv_sec);
		stamp_msec = (tvtime.tv_usec - app.toffset.tv_usec)/1000;

		if (stamp_msec < 0) {
			stamp_sec--;
			stamp_msec += 1000;
		};

		/* Check if this packet should be added to the last one */
		if (config.jointimeout && pipe->charbased) {
			static int oldmsec = -1;
			static unsigned oldsec = (unsigned) -1;
			int ossec;
			int osmsec;
			int join = 0;

			osmsec =  stamp_msec - oldmsec;
			ossec = stamp_sec - oldsec;
			if (osmsec < 0) {
				ossec--;
				osmsec += 1000;
			}
			
			if ((config.jointimeout/1000) == ossec) {
				if ((config.jointimeout % 1000) > osmsec) {
					join = 1;
				} else {
					join = 0;
				}
			} else if ((config.jointimeout/1000) > ossec) {
				join = 1;
			} else {
				join = 0;
			}

			if (join) {
				logfile->addnewline = 0;
				writetimestamp = 0;
			}

			if (config.logreltime && writetimestamp) {
				oldmsec = 0;
				oldsec = 0;
			} else {
				oldmsec = stamp_msec;
				oldsec = stamp_sec;
			}
		}

		if (config.logreltime && writetimestamp)
			memcpy(&app.toffset, &tvtime, sizeof(struct timeval));
	}

	/* Align all single writes to a new line if we are in charbased mode
	 * and alignlog is active */
	if (pipe->charbased) {
		/* Check if we should add a newline right now */
		if (logfile->addnewline && config.alignlog)
			if (write(logfile->fh, "\n", 1) == -1)
				goto failure_exit;

		/* Check if we should add a newline next time */
		if (pipe->buf[pipe->bfill-1] != '\n') {
			logfile->addnewline = 1;
		} else {
			logfile->addnewline = 0;
		}
	}


	/* Write timestamp */
	if (writetimestamp) {
		char timestamp[16];
		int len;

		/* Normally use a 9 digit timestamp but extend the size to 13
		 * digits if needed */
		if (stamp_sec < 999999) {
			len = snprintf(timestamp, 12, "%06u%03u: ", stamp_sec,
				 stamp_msec);
			if (len < 0)
				goto failure_exit;
			prefixlen = 11;
		} else {
			len = snprintf(timestamp, 16, "%010u%03u: ", stamp_sec,
				 stamp_msec);
			if (len < 0)
				goto failure_exit;
			prefixlen = 15;
		}
		if (write(logfile->fh, timestamp, prefixlen) == -1)
			goto failure_exit;
	}

	/* Write prefix */
	if (pipe->lineprefix != NULL) {
		if (write(logfile->fh, pipe->lineprefix, pipe->lineprefixlen)
		    == -1) {
			goto failure_exit;
		}
		prefixlen += pipe->lineprefixlen;
	}

	int i = 0;
	int offset = 0;

	if (config.alignlinebreaks && pipe->charbased) {
		while(pipe->linebreakpos[i]) {
			if (i) {
				if (write(logfile->fh, logfile->indent,
					  prefixlen) == -1) {
					goto failure_exit;
				}
			}
			if (write(logfile->fh, pipe->buf+offset,
				  pipe->linebreakpos[i]-offset) == -1) {
				goto failure_exit;
			}
			offset = pipe->linebreakpos[i];
			i++;
		}
		for (i=offset; i<pipe->bfill; i++) {
			if (pipe->buf[i]=='\n' || (i+1==pipe->bfill)) {
				if (offset) {
					if (write(logfile->fh,
						  logfile->indent,
						  prefixlen) == -1) {
						goto failure_exit;
					}
				}
				if (write(logfile->fh, pipe->buf + 
					  offset, i-offset + 1)
				    == -1) {
					goto failure_exit;
				}
				offset = i+1;
			}
		}
	} else {
		/* Write data */
		if (write(logfile->fh, pipe->buf, pipe->bfill) == -1) {
			goto failure_exit;
		}
	}

	if (CONFIG_USE_THREADS)
		pthread_mutex_unlock(&logfile_mutex);

	return 0;

failure_exit:
	if (CONFIG_USE_THREADS)
		pthread_mutex_unlock(&logfile_mutex);
	return -1;
}

int get_lock(logfile_t* logfile)
{
	struct flock	lock;

	if (!config.locklogfile)
		return 0;

	lock.l_type	= F_WRLCK;
	lock.l_start	= 0;
	lock.l_whence	= SEEK_SET;
	lock.l_len	= 0;

	return fcntl(logfile->fh, F_SETLK, &lock);
}

int logfile_open(char* filename, logfile_t* logfile)
{
	int	flags;
	char*	altname = NULL;

	if (filename == NULL)
		return 0;

	logfile->name = get_longpath(filename);
	if (logfile->name == NULL)
		return -1;

	flags = O_CREAT | O_WRONLY | O_APPEND;

	while (logfile->appendnr < config.maxlogfiles) {
		if (altname != NULL)
			free(altname);

		altname = logfile_get_alternate_name(logfile);
		if (logfile->name == NULL)
			return -1;

		if (!config.appendlog) {
			/* Check if we can rename the old logfile, or if it is
			 * still locked by a running logapp session */
			logfile->fh = open(altname, O_WRONLY, 0644);
			if (logfile->fh != -1) {
				if (!get_lock(logfile)) {
					close(logfile->fh);
					if (logfile_rename(logfile, altname)) {
						/* unable to rename logfile
						   -> try next slot */
						logfile->fh = -1;
						logfile->appendnr++;
						continue;
					}
				} else {
					/* it is locked, so try next slot */
					close(logfile->fh);
					logfile->fh = -1;
					logfile->appendnr++;
					continue;
				}
			}
		}

		logfile->fh = open(altname, flags, 0644);
		if (get_lock(logfile)) {
			close(logfile->fh);
			logfile->fh = -1;
			logfile->appendnr++;
			continue;
		}
		break;
	}

	/* We have to replace the original filename with the new one, in
	 * case the lock detection forced us to change the name */
	free(logfile->name);
	logfile->name = altname;

	if (logfile->fh == -1) {
		if (logfile->appendnr == config.maxlogfiles) {
			error("no possible logfile is writeable\n");
			error("-> check permissions or try increasing the "
			      "maxaltlogfiles value\n");
		}
		return -1;
	}

	if (logfile->appendnr && config.warnlogfilelock) {
		warning("using %s as logfile\n", logfile->name);
		warning("most likely another logapp session is locking "
			"the logfile\n");
	}

	if (!config.appendlog) {
		if (ftruncate(logfile->fh, 0) == -1) {
			error("Problem truncating logfile (errno = %d)\n",
			      errno);
			return -1;
		}
	}

	if (logfile_add_header(logfile)) {
		error("problem writing log header to file\n");
		return -1;
	}

	return 0;
}

int logfile_close(logfile_t* logfile)
{
	if (!logfile->name)
		return 0;

	if (logfile_add_footer(logfile)) {
		error("problem writing log footer to file\n");
		return -1;
	}

	close(logfile->fh);

	return 0;
}
