#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <string>
#include <vector>

#include "tlse.h"

#include "log.h"

using namespace std;

// Yes, it's a bit ugly.
#define SYSLOG_FAKE_FILE (static_cast<FILE *>(nullptr))

bool logging_started = false;
vector<FILE *> log_destinations;

void add_log_destination_file(const string &filename)
{
	FILE *fp = fopen(filename.c_str(), "a");
	if (fp == nullptr) {
		perror(filename.c_str());
		return;
	}

	log_destinations.push_back(fp);	
}

void add_log_destination_console()
{
	log_destinations.push_back(stderr);
}

void add_log_destination_syslog()
{
	openlog("cubemap", LOG_PID, LOG_DAEMON);
	log_destinations.push_back(SYSLOG_FAKE_FILE);
}

void start_logging()
{
	logging_started = true;
}

void shut_down_logging()
{
	for (size_t i = 0; i < log_destinations.size(); ++i) {
		if (log_destinations[i] == SYSLOG_FAKE_FILE) {
			closelog();
		} else if (log_destinations[i] != stderr) {
			if (fclose(log_destinations[i]) != 0) {
				perror("fclose");
			}
		}
	}
	log_destinations.clear();
	logging_started = false;
}

void log(LogLevel log_level, const char *fmt, ...)
{
	char formatted_msg[4096];
	va_list ap;
	va_start(ap, fmt);
	vsnprintf(formatted_msg, sizeof(formatted_msg), fmt, ap);
	va_end(ap);

	time_t now = time(nullptr);
	struct tm lt;
	struct tm *ltime = localtime_r(&now, &lt);
	char timestamp[1024];
	if (ltime == nullptr) {
		strcpy(timestamp, "???");
	} else {
		strftime(timestamp, sizeof(timestamp), "%a, %d %b %Y %T %z", ltime);
	}

	const char *log_level_str;
	int syslog_level;

	switch (log_level) {
	case INFO:
		log_level_str = "INFO:    ";
		syslog_level = LOG_INFO;
		break;
	case WARNING:
		log_level_str = "WARNING: ";
		syslog_level = LOG_WARNING;
		break;
	case ERROR:
		log_level_str = "ERROR:   ";
		syslog_level = LOG_ERR;
		break;
	default:
		assert(false);
	}

	// Log to stderr if logging hasn't been set up yet. Note that this means
	// that such messages will come even if there are no “error_log” lines.
	if (!logging_started) {
		fprintf(stderr, "[%s] %s%s\n", timestamp, log_level_str, formatted_msg);
		return;
	}

	for (size_t i = 0; i < log_destinations.size(); ++i) {
                if (log_destinations[i] == SYSLOG_FAKE_FILE) {
			syslog(syslog_level, "%s", formatted_msg);
		} else {
			int err = fprintf(log_destinations[i], "[%s] %s%s\n", timestamp, log_level_str, formatted_msg);
			if (err < 0) {
				perror("fprintf");
			}
			if (log_destinations[i] != stderr) {
				fflush(log_destinations[i]);
			}
		}
	}
}

void log_perror(const char *msg)
{
	char errbuf[4096];
	log(ERROR, "%s: %s", msg, strerror_r(errno, errbuf, sizeof(errbuf)));
}

void log_tls_error(const char *msg, int tls_err)
{
	switch (tls_err) {
	case TLS_NEED_MORE_DATA:
		log(ERROR, "%s: Need more data (TLS)", msg);
		break;
	case TLS_GENERIC_ERROR:
		log(ERROR, "%s: Generic TLS error", msg);
		break;
	case TLS_BROKEN_PACKET:
		log(ERROR, "%s: Broken TLS packet", msg);
		break;
	case TLS_NOT_UNDERSTOOD:
		log(ERROR, "%s: Not understood (TLS)", msg);
		break;
	case TLS_NOT_SAFE:
		log(ERROR, "%s: Not safe (TLS)", msg);
		break;
	case TLS_NO_COMMON_CIPHER:
		log(ERROR, "%s: No common TLS cipher", msg);
		break;
	case TLS_UNEXPECTED_MESSAGE:
		log(ERROR, "%s: Unexpected TLS message", msg);
		break;
	case TLS_CLOSE_CONNECTION:
		log(ERROR, "%s: Close TLS connection", msg);
		break;
	case TLS_COMPRESSION_NOT_SUPPORTED:
		log(ERROR, "%s: TLS compression not supported", msg);
		break;
	case TLS_NO_MEMORY:
		log(ERROR, "%s: No TLS memory", msg);
		break;
	case TLS_NOT_VERIFIED:
		log(ERROR, "%s: Not verified (TLS)", msg);
		break;
	case TLS_INTEGRITY_FAILED:
		log(ERROR, "%s: TLS integrity failed", msg);
		break;
	case TLS_ERROR_ALERT:
		log(ERROR, "%s: TLS alert", msg);
		break;
	case TLS_BROKEN_CONNECTION:
		log(ERROR, "%s: Broken TLS connection", msg);
		break;
	case TLS_BAD_CERTIFICATE:
		log(ERROR, "%s: Bad TLS certificate", msg);
		break;
	case TLS_UNSUPPORTED_CERTIFICATE:
		log(ERROR, "%s: Unsupported TLS certificate", msg);
		break;
	case TLS_NO_RENEGOTIATION:
		log(ERROR, "%s: No TLS renegotiation", msg);
		break;
	case TLS_FEATURE_NOT_SUPPORTED:
		log(ERROR, "%s: TLS feature not supported", msg);
		break;
	default:
		log(ERROR, "%s: Unknown TLS error %d", msg, tls_err);
		break;
	}
}
