/*  
    EQ2Emulator:  Everquest II Server Emulator
    Copyright (C) 2007  EQ2EMulator Development Team (http://www.eq2emulator.net)

    This file is part of EQ2Emulator.

    EQ2Emulator 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 3 of the License, or
    (at your option) any later version.

    EQ2Emulator 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
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with EQ2Emulator.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "Log.h"
#include "xmlParser.h"
#include "types.h"
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <sys/stat.h>
#include "../WorldServer/World.h"
#include "../WorldServer/client.h"
#include "../WorldServer/zoneserver.h"

extern ZoneList zone_list;

#ifdef _WIN32
	#include <process.h>
	#ifndef snprintf
		#define snprintf sprintf_s
	#endif
#include <WinSock2.h>
	#include <Windows.h>
#else
#endif

#define LOG_CATEGORY(category) #category,
const char *log_category_names[NUMBER_OF_LOG_CATEGORIES] = {
	#include "LogTypes.h"
};

#define LOG_TYPE(category, type, level, color, enabled, logfile, console, client, str) { level, color, enabled, logfile, console, client, LOG_ ##category, #category "__" #type, ( strlen(str)>0 ) ? str : #category "__" #type },
static LogTypeStatus real_log_type_info[NUMBER_OF_LOG_TYPES+1] =
{
	#include "LogTypes.h"
	{ 0, 0, false, false, false, false, NUMBER_OF_LOG_CATEGORIES, "BAD TYPE", "Bad Name" } /* dummy trailing record */
};

LogTypeStatus *log_type_info = real_log_type_info;

//make these rules?
#define LOG_CYCLE		100		//milliseconds between each batch of log writes
#define LOGS_PER_CYCLE	50		//amount of logs to write per cycle

#define LOG_DIR	"logs"

#if defined LOGIN
#define EXE_NAME	"login"
#elif defined WORLD
#define EXE_NAME	"world"
#elif defined PARSER
#define EXE_NAME	"parser"
#elif defined PATCHER
#define EXE_NAME	"patcher"
#else
#define EXE_NAME	"whatprogyourunning"
#endif

#define DATE_MAX		8
#define LOG_NAME_MAX		32

struct logq_t {
	LogType log_type;
	char date[DATE_MAX + 1];
	char name[LOG_NAME_MAX + 1];
	char *text;
	struct logq_t *next;
	struct logq_t *prev;
};

//doubly linked list of logs
static logq_t head;
static logq_t tail;
static int num_logqs = 0;
static Mutex mlogqs;

//loop until....
static bool looping = false;

//because our code has LogWrite's before main(), make sure any of those do the
//call to LogStart if it hasn't been called already...
static bool start_called = false;

static void SetConsoleColor(int color) {
#ifdef _WIN32
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);

	if (handle == NULL || handle == INVALID_HANDLE_VALUE)
		return;
#endif

	switch (color) {
		case FOREGROUND_WHITE:
		case FOREGROUND_WHITE_BOLD:
		case FOREGROUND_RED:
		case FOREGROUND_RED_BOLD:
		case FOREGROUND_GREEN:
		case FOREGROUND_GREEN_BOLD:
		case FOREGROUND_BLUE:
		case FOREGROUND_BLUE_BOLD:
		case FOREGROUND_YELLOW:
		case FOREGROUND_YELLOW_BOLD:
		case FOREGROUND_CYAN:
		case FOREGROUND_CYAN_BOLD:
		case FOREGROUND_MAGENTA:
		case FOREGROUND_MAGENTA_BOLD:
#ifdef _WIN32
			SetConsoleTextAttribute(handle, color);
#else
			printf("\033[%i;%i;40m", color > 100 ? 1 : 0, color > 100 ? color - 100 : color);
#endif
			break;
		default:
#ifdef _WIN32
			SetConsoleTextAttribute(handle, FOREGROUND_WHITE_BOLD);
#else
			printf("\033[0;37;40m");
#endif
			break;
	}
}

static FILE * OpenLogFile() {
	char file[FILENAME_MAX + 1];
	struct stat st;
	struct tm *tm;
	time_t now;
	FILE *f;

	now = time(NULL);
	tm = localtime(&now);

	//make sure the logs directory exists
	if (stat(LOG_DIR, &st) != 0) {
#ifdef _WIN32
		if (!CreateDirectory(LOG_DIR, NULL)) {
			fprintf(stderr, "Unable to create directory '%s'\n", LOG_DIR);
			return stderr;
		}
#else
		if (mkdir(LOG_DIR, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) {
			fprintf(stderr, "Unable to create direcotry '%s': %s\n", LOG_DIR, strerror(errno));
			return stderr;
		}
#endif
	}

#ifdef NO_PIDLOG
	snprintf(file, FILENAME_MAX, LOG_DIR"/%04i-%02i-%02i_eq2" EXE_NAME ".log", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
#else
	snprintf(file, FILENAME_MAX, LOG_DIR"/%04i-%02i-%02i_eq2" EXE_NAME "_%04i.log", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, getpid());
#endif

	if ((f = fopen(file, "a")) == NULL) {
		fprintf(stderr, "Could not open '%s' for writing: %s\n", file, strerror(errno));
		return stderr;
	}

	return f;
}

static void WriteQueuedLogs(int count) {
	logq_t pending_head, pending_tail, *logq, *tmp;
	int i = 0;
	FILE *f;

	pending_head.next = &pending_tail;
	pending_tail.prev = &pending_head;

	//loop through our queued logs and store at most `count` logs into a temporary list
	//since io functions are expensive, we'll write from a temporary list so we don't hold the
	//write lock of the main list for a long period of time
	mlogqs.writelock();
	logq = head.next;

	while (head.next != &tail) {
		//first remove the log from the master list
		logq = head.next;
		logq->next->prev = &head;
		head.next = logq->next;

		//now insert it into the temporary list
		tmp = pending_tail.prev;
		tmp->next = logq;
		logq->prev = tmp;
		logq->next = &pending_tail;
		pending_tail.prev = logq;
		--num_logqs;

		logq = logq->next;

		//if we have a limit, check it
		if (count > 0 && ++i == count)
			break;	
	}

	mlogqs.releasewritelock();

	//if we have no logs to write, we're done
	if ((logq = pending_head.next) == &pending_tail)
		return;

	while (logq != &pending_tail) {
		if (log_type_info[logq->log_type].console) {
			SetConsoleColor(FOREGROUND_WHITE_BOLD);
			printf("%s ", logq->date);
			SetConsoleColor(log_type_info[logq->log_type].color);
			printf("%s ", log_type_info[logq->log_type].display_name);
			SetConsoleColor(FOREGROUND_WHITE_BOLD);
			printf("%-10s: ", logq->name);
			SetConsoleColor(log_type_info[logq->log_type].color);
			printf("%s\n", logq->text);
			SetConsoleColor(-1);
			fflush(stdout);
		}

		if (log_type_info[logq->log_type].logfile) {
			f = OpenLogFile();

			if (f != stderr || (f == stderr && !log_type_info[logq->log_type].console)) {
				fprintf(f, "%s %s %s: %s\n", logq->date, log_type_info[logq->log_type].display_name, logq->name, logq->text);
				fflush(f);
				if (f != stderr)
					fclose(f);
			}
		}

#if defined WORLD
		if (log_type_info[logq->log_type].client) {
			// eventually output logging to the client who "subscribed" to the logger
			// in-game, they type:
			//		/logsys add WORLD__DEBUG 5
			// to watch world debug loggers of level 5 or less
		}
#endif

		tmp = logq;
		logq = logq->next;

		free(tmp->text);
		free(tmp);
	}
}

ThreadReturnType LogLoop(void *args) {
	while (looping) {
		WriteQueuedLogs(LOGS_PER_CYCLE);
		Sleep(LOG_CYCLE);
	}

	THREAD_RETURN(NULL);
}

void LogStart() {
	if (start_called)
		return;

	//initialize the doubly linked list
	head.prev = NULL;
	head.next = &tail;
	tail.prev = &head;
	tail.next = NULL;

	mlogqs.SetName("logqueue");
	looping = true;

#ifdef _WIN32
	_beginthread(LogLoop, 0, NULL);
#else
	pthread_t thread;
	pthread_create(&thread, NULL, LogLoop, NULL);
	pthread_detach(thread);
#endif

	start_called = true;
}

void LogStop() {
	looping = false;
	WriteQueuedLogs(-1);
	start_called = false;
}

static void LogQueueAdd(LogType log_type, char *text, int len, const char *cat_text = NULL) {
	logq_t *logq;
	struct tm *tm;
	time_t now;

	if ((logq = (logq_t *)calloc(1, sizeof(logq_t))) == NULL) {
		free(text);
		fprintf(stderr, "%s: %u: Unable to allocate %zu bytes\n", __FUNCTION__, __LINE__, sizeof(logq_t));
		return;
	}

	if ((logq->text = (char *)calloc(len + 1, sizeof(char))) == NULL) {
		free(text);
		free(logq);
		fprintf(stderr, "%s: %u: Unable to allocate %i bytes\n", __FUNCTION__, __LINE__, len + 1);
		return;
	}

	now = time(NULL);
	tm = localtime(&now);

	logq->log_type = log_type;
	snprintf(logq->date, DATE_MAX + 1, "%02i:%02i:%02i", tm->tm_hour, tm->tm_min, tm->tm_sec);
	strncpy(logq->name, cat_text == NULL || cat_text[0] == '\0' ? log_type_info[log_type].name : cat_text, LOG_NAME_MAX);
	strncpy(logq->text, text, len);
	free(text);

	if (!start_called)
		LogStart();

	//insert at the end
	mlogqs.writelock();
	tail.prev->next = logq;
	logq->prev = tail.prev;
	logq->next = &tail;
	tail.prev = logq;
	++num_logqs;
	mlogqs.releasewritelock();
}

int8 GetLoggerLevel(LogType type)
{
	return log_type_info[type].level;
}

// JA: horrific hack for Parser, since queued logging keeps crashing between parses.
#ifndef PARSER
void LogWrite(LogType type, int8 log_level, const char *cat_text, const char *fmt, ...) {
	int count, size = 64;
	char *buf;
	va_list ap;

	// if there is no formatting, or the logger is DISABLED
	// or the log_level param exceeds the minimum allowed value, abort logwrite
	if (!log_type_info[type].enabled || (log_level > 0 && log_type_info[type].level < log_level))
		return;

	while (true) {
		if ((buf = (char *)malloc(size)) == NULL) {
			fprintf(stderr, "%s: %i: Unable to allocate %i bytes\n", __FUNCTION__, __LINE__, size);
			return;
		}
		
		va_start(ap, fmt);
		count = vsnprintf(buf, size, fmt, ap);
		va_end(ap);

		if (count > -1 && count < size)
			break;

		free(buf);
		if (count > 1)
			size = count + 1;
		else
			size *= 2;
	}

	LogQueueAdd(type, buf, count, cat_text);
}
#else
void LogWrite(LogType type, int8 log_level, const char *cat_text, const char *format, ...) 
{
	// if there is no formatting, or the logger is DISABLED
	// or the log_level param exceeds the minimum allowed value, abort logwrite
	if ( !format || !log_type_info[type].enabled || (log_level > 0 && log_type_info[type].level < log_level) )
		return;

    time_t clock;
    struct tm *tm;

	char buffer[LOG_BUFFER_SIZE], date[32];
	va_list args;
	FILE *f;
	size_t cat_text_len = 0;

	memset(buffer, 0, sizeof(buffer));
	memset(date, 0, sizeof(date));

	va_start(args, format);
	vsnprintf(buffer, sizeof(buffer) - 1, format, args);
	va_end(args);

    time(&clock);
    tm = localtime(&clock);
	snprintf(date, sizeof(date)-1, "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
	// DateString(date, sizeof(date));

	cat_text_len = strlen(cat_text);
	//if( strlen(cat_text) == 0 ) // cat_text was blank
	//	cat_text = (char*)log_type_info[type].name;

	/* write to the log file? */
	if (log_type_info[type].logfile) 
	{
		char exename[200] = "";

	#ifdef LOGIN
		snprintf(exename, sizeof(exename), "login");
	#endif
	#ifdef WORLD
		snprintf(exename, sizeof(exename), "world");
	#endif
	#ifdef PARSER
		snprintf(exename, sizeof(exename), "parser");
	#endif
	#ifdef PATCHER
		snprintf(exename, sizeof(exename), "patcher");
	#endif

		char filename[200], log_header[200] = "";

	#ifndef NO_PIDLOG
		snprintf(filename, sizeof(filename)-1, "logs/%04d-%02d-%02d_eq2%s_%04i.log", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, exename, getpid());
	#else
		snprintf(filename, sizeof(filename)-1, "logs/%04d-%02d-%02d_eq2%s.log", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, exename);
	#endif

		f=fopen(filename, "r");
		if( !f )
			snprintf(log_header, sizeof(log_header), "===[ New log '%s' started ]===\n\n", filename);
		else
		fclose (f);

		f = fopen(filename, "a");
		if (f) {
			if( strlen(log_header) > 0 )
				fprintf(f, "%s\n", log_header);
			fprintf(f, "%s %s %s: %s\n", date, log_type_info[type].display_name, cat_text, buffer);
			fclose(f);
		}
	}
	
	/* write to the console? */
	if (log_type_info[type].console)
	{
	#ifdef _WIN32
			ColorizeLog(log_type_info[type].color, date, log_type_info[type].display_name, cat_text_len == 0 ? log_type_info[type].name : cat_text, (string)buffer);
	#else
				printf("%s %s %s: %s\n", date, log_type_info[type].display_name, cat_text_len == 0 ? log_type_info[type].name : cat_text, buffer);
	#endif
	}
}

void
ColorizeLog(int color, char *date, const char *display_name, const char *category, string buffer)
{
	#ifdef _WIN32
		HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
		if (console == INVALID_HANDLE_VALUE) {
			printf("%s %s %s: %s\n", date, display_name, category, buffer);
			return;
		}
		printf("%s ", date);
		SetConsoleTextAttribute(console, color);
		printf("%s ", display_name);
		SetConsoleTextAttribute(console, FOREGROUND_WHITE_BOLD);
		printf("%s: ", category);
		SetConsoleTextAttribute(console, color);
		printf("%s\n", buffer.c_str());
		SetConsoleTextAttribute(console, FOREGROUND_WHITE);
	#endif
}

#endif

LogTypeStatus *
GetLogTypeStatus(const char *category, const char *type) {
	char combined[256];
	int i;

	memset(combined, 0, sizeof(combined));
	snprintf(combined, sizeof(combined) - 1, "%s__%s", category, type);

	for (i = 0; i < NUMBER_OF_LOG_TYPES; i++) {
		if (strcasecmp(log_type_info[i].name, combined) == 0)
			return &log_type_info[i];
	}

	return &log_type_info[NUMBER_OF_LOG_TYPES];
}

void
ProcessLogConfig(XMLNode node) {
	int i;
	const char *category, *type, *level, *color, *enabled, *logs;
	LogTypeStatus *lfs;
	XMLNode child;

	category = node.getAttribute("Category");
	if (!category) {
		LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Config missing a Category");
		return;
	}

	for (i = 0; i < node.nChildNode("ConfigType"); i++) {
		child = node.getChildNode("ConfigType", i);
		type = child.getAttribute("Type");

		if (!type) {
			LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Config missing a Type");
			continue;
		}

		lfs = GetLogTypeStatus(category, type);
		level = child.getAttribute("Level");
		enabled = child.getAttribute("Enabled");
		color = child.getAttribute("Color");
		logs = child.getAttribute("Logs");

		if (!logs) {
			LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Config missing 'Logs' attribute to specify which log(s) to write to");
			continue;
		}
		if (!IsNumber(logs)) {
			LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Attribute 'Logs' must be a number. See LogTypes.h for the valid types.");
			continue;
		}

		if (enabled) {
			if (!strcasecmp("true", enabled) || !strcasecmp("on", enabled))
				lfs->enabled = true;
			else if (!strcasecmp("false", enabled) || !strcasecmp("off", enabled))
				lfs->enabled = false;
			else
				LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Log setting 'Enabled' has invalid value '%s'. 'true'/'on' or 'false'/'off' are valid values", enabled);
		}

		if (IsNumber(level))
			lfs->level = atoi(level);
		else
			lfs->level = 0;

		if (color) {
			if (IsNumber(color))
				lfs->color = atoi(color);
			else if (!strcasecmp("White", color))
				lfs->color = FOREGROUND_WHITE;
			else if (!strcasecmp("Green", color))
				lfs->color = FOREGROUND_GREEN;
			else if (!strcasecmp("Yellow", color))
				lfs->color = FOREGROUND_YELLOW;
			else if (!strcasecmp("Red", color))
				lfs->color = FOREGROUND_RED;
			else if (!strcasecmp("Blue", color))
				lfs->color = FOREGROUND_BLUE;
			else if (!strcasecmp("Cyan", color))
				lfs->color = FOREGROUND_CYAN;
			else if (!strcasecmp("Magenta", color))
				lfs->color = FOREGROUND_MAGENTA;
			else if (!strcasecmp("WhiteBold", color))
				lfs->color = FOREGROUND_WHITE_BOLD;
			else if (!strcasecmp("GreenBold", color))
				lfs->color = FOREGROUND_GREEN_BOLD;
			else if (!strcasecmp("YellowBold", color))
				lfs->color = FOREGROUND_YELLOW_BOLD;
			else if (!strcasecmp("RedBold", color))
				lfs->color = FOREGROUND_RED_BOLD;
			else if (!strcasecmp("BlueBold", color))
				lfs->color = FOREGROUND_BLUE_BOLD;
			else if (!strcasecmp("CyanBold", color))
				lfs->color = FOREGROUND_CYAN_BOLD;
			else if (!strcasecmp("MagentaBold", color))
				lfs->color = FOREGROUND_MAGENTA_BOLD;
			else
				LogWrite(MISC__WARNING, 0, "Misc", "Error parsing log config. Log setting 'Color' has invalid value '%s'", color);
		}

		// JA: something was wrong here, lfs->logfile or console always was true, even if bit was off. Will ask Scatman about it someday.
		lfs->logfile	= (atoi(logs) & LOG_LOGFILE);
		lfs->console	= (atoi(logs) & LOG_CONSOLE);
		lfs->client		= (atoi(logs) & LOG_CLIENT);
	}
}

bool
LogParseConfigs() {
	XMLNode main_node;
	int i;

	main_node = XMLNode::openFileHelper("log_config.xml", "EQ2EmuLogConfigs");
	if (main_node.isEmpty()) {
		LogWrite(MISC__WARNING, 0, "Misc", "Unable to parse the file 'log_config.xml' or it does not exist. Default values will be used");
		return false;
	}

	for (i = 0; i < main_node.nChildNode("LogConfig"); i++)
		ProcessLogConfig(main_node.getChildNode("LogConfig", i));

	return true;
}
