#ifdef __linux__
 #include <sys/epoll.h>
#else
 #include <sys/event.h>
#endif
#include <sys/stat.h>
#include <sys/socket.h>

#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <ctype.h>
#include <err.h>
#include <fcntl.h>
#include <pthread.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <MagickWand/MagickWand.h>

#include "config.h"

#define NUM_IMAGES      (sizeof(imgpaths) / sizeof(imgpaths[0]))

typedef struct {
	struct sockaddr_in addr;
	int sock;
} ThreadArgs;

typedef struct {
	ThreadArgs clients[MAX_QUEUE];
	int head;
	int tail;
	int count;
	pthread_mutex_t lock;
	pthread_cond_t  note;
} ClientQueue;

typedef struct {
	unsigned char *data;
	size_t size;
} ImageBuffer;

static void seedinit(void);
static int imgloadbuff(const char *, ImageBuffer *);
static int imgpreload(void);
static char *b64e(const unsigned char *, size_t, size_t *);
static char *fontpreload(void);
static double randrange(double, double);
static unsigned long randnext(void);
static unsigned char *imggen(const unsigned char *, size_t,
                             const char *, size_t *,
		             double, double, double);
static void terminatepath(char *, size_t);
static char *getheader(const char *, const char *);
static void handleclient(ThreadArgs *);
static void *worker(void *);

static ClientQueue queue;
static ImageBuffer imgbuffs[NUM_IMAGES];
static char *fonturi;
/* TODO: remove compiler-specific __thread */
static __thread char buffer[BUFFER_SIZE];
static __thread char method[16], path[256], header[512];
static unsigned long randstate = 0;
static pthread_mutex_t random_mutex = PTHREAD_MUTEX_INITIALIZER;

void
seedinit(void)
{
	int fd;

	/* Try urandom first, random as a fallback */
	if ((fd = open("/dev/urandom", O_RDONLY)) < 0) {
		fd = open("/dev/random", O_RDONLY);
	}

	if (fd < 0) {
		warn("open random device");
		randstate = 12345;   /* Fallback seed */
		return;
	}

	if (read(fd, &randstate, sizeof(randstate)) <
	         (ssize_t)sizeof(randstate)) {
		warn("read random device");
		randstate = 12345;   /* Fallback seed */
	}

	close(fd);
	printf("Random seed initialized: %lu\n", randstate);

	return;
}

int
imgloadbuff(const char *path, ImageBuffer *ibuffer)
{
	int fd;
	ssize_t bytesr;
	struct stat st;

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		warn("open image file");
		return -1;
	}

	if (fstat(fd, &st) < 0) {
		warn("fstat");
		close(fd);
		return -1;
	}

	ibuffer->size = st.st_size;
	ibuffer->data = malloc(ibuffer->size);
	if (!ibuffer->data) {
		warnx("failed to allocate image buffer");
		close(fd);
		return -1;
	}

	bytesr = read(fd, ibuffer->data, ibuffer->size);
	if (bytesr != (ssize_t)ibuffer->size) {
		warn("read image file");
		free(ibuffer->data);
		ibuffer->data = NULL;
		ibuffer->size = 0;
		close(fd);
		return -1;
	}

	close(fd);
	printf("Loaded image: %s (%zu bytes)\n", path, ibuffer->size);

	return 0;
}


int
imgpreload(void)
{
	for (int i = 0; i < (int)NUM_IMAGES; i++) {
		if (imgloadbuff(imgpaths[i], &imgbuffs[i]) < 0)
		{
			warnx("failed to load image %d: %s", i,
			      imgpaths[i]);
			return -1;
		}
	}

	return 0;
}

char *
b64e(const unsigned char *data, size_t ilen, size_t *olen)
{
	char *edata;
	uint32_t octet_a, octet_b, octet_c, triple;
	static const char etable[] =
		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	*olen = 4 * ((ilen + 2) / 3);

	edata = malloc(*olen + 1);
	if (!edata) return NULL;

	for (size_t i = 0, j = 0; i < ilen;) {
		octet_a = i < ilen ? data[i++] : 0;
		octet_b = i < ilen ? data[i++] : 0;
		octet_c = i < ilen ? data[i++] : 0;

		triple = (octet_a << 0x10) + (octet_b << 0x08) +
				  octet_c;

		edata[j++] = etable[(triple >> 3 * 6) & 0x3F];
		edata[j++] = etable[(triple >> 2 * 6) & 0x3F];
		edata[j++] = (i > ilen + 1) ? '=' : etable[(triple >> 1 * 6) &
							   0x3F];
		edata[j++] = (i > ilen) ? '=' : etable[triple & 0x3F];
	}

	edata[*olen] = '\0';

	return edata;
}

char *
fontpreload(void)
{
	size_t fontsz, b64sz, readsz;
	FILE *file;
	char *b64font, *inlineuri;
	unsigned char *fontbuffer;

	file = fopen(fontpath, "rb");
	if (!file)
		err(1, "open font file");

	fseek(file, 0, SEEK_END);
	fontsz = ftell(file);
	fseek(file, 0, SEEK_SET);

	fontbuffer = malloc(fontsz);
	if (!fontbuffer)
		errx(1, "failed to allocate raw font");

	readsz = fread(fontbuffer, 1, fontsz, file);
	if (readsz < fontsz || ferror(file))
		err(1, "read font file");

	fclose(file);

	b64font = b64e(fontbuffer, fontsz, &b64sz);
	free(fontbuffer);

	inlineuri = malloc(b64sz + 8);
	if (!inlineuri)
		errx(1, "failed to allocate base64 font");

	sprintf(inlineuri, "inline:%s", b64font);
	free(b64font);

	printf("Loaded font from: %s\n", fontpath);

	return inlineuri;
}

/* Linear congruential generator */
unsigned long
randnext(void)
{
	unsigned long result;

	pthread_mutex_lock(&random_mutex);
	randstate = (randstate * 1103515245 + 12345) & 0x7fffffff;
	result = randstate;
	pthread_mutex_unlock(&random_mutex);

	return result;
}

double
randrange(double min, double max)
{
	unsigned long rand_val;
	double normalized;

	rand_val = randnext();
	normalized = (double)rand_val / 0x7fffffff;

	return min + (normalized * (max - min));
}

unsigned char *
imggen(const unsigned char *imgdata, size_t imgsz,
       const char *msg, size_t *output_size,
       double hue, double saturation, double brightness)
{
	size_t data_size = 0;
	unsigned char *result = NULL;

	MagickWand *wand = NewMagickWand();
	DrawingWand *dw = NewDrawingWand();
	PixelWand *pw = NewPixelWand();

	if (!MagickReadImageBlob(wand, imgdata, imgsz)) {
		fprintf(stderr, "failed to read image from buffer");
		goto cleanup;
	}

	MagickModulateImage(wand, brightness, saturation, hue);

	PixelSetRed(pw, randrange(0.0, 1.0));
	PixelSetGreen(pw, randrange(0.0, 1.0));
	PixelSetBlue(pw, randrange(0.0, 1.0));
	PixelSetAlpha(pw, 1.0); /* Fully opaque */

	DrawSetFillColor(dw, pw);
	DrawSetFont(dw, fonturi);
	DrawSetFontSize(dw, (int)randrange(8.0, 200.0));
	DrawSetGravity(dw, CenterGravity);

	MagickAnnotateImage(wand, dw, saturation / 4, hue / 4, hue, msg);
	result = MagickGetImageBlob(wand, &data_size);

cleanup:
	*output_size = data_size;
	DestroyMagickWand(wand);
	DestroyDrawingWand(dw);
	DestroyPixelWand(pw);

	return result;
}

void
terminatepath(char *path, size_t len)
{
	char *current = path;

	while (*current && *current != '\r' && current < path + len)
		current++;

	*current = '\0';

	return;
}

char *
getheader(const char *headerbuff, const char *find)
{
	if (!headerbuff) {
		return NULL;
	}

	const size_t tlen = strlen(find);
	char *current = (char *)headerbuff;

	while (*current) {
		if (strncasecmp(current, find, tlen) == 0) {
			char *vptr, *endptr;

			vptr = current + tlen;
			while (*vptr == ' ' || *vptr == '\t') {
				vptr++;
			}

			endptr = strchr(vptr, '\r');
			if (endptr) *endptr = '\0';

			return vptr;
		}

		current = strchr(current, '\n');
		if (!current) {
			break;
		}
		current++;
	}

	return NULL;
}

void
handleclient(ThreadArgs *args)
{
	int clientsock;
	ssize_t recbytes;
	static const char response_404[] = "HTTP/1.1 404 Not Found\r\n"
				   "Content-Type: text/html\r\n"
				   "Content-Length: 48\r\n\r\n"
				   "<html><body><h1>404 Not Found</h1></body></html>";

	clientsock = args->sock;

	recbytes = recv(clientsock, buffer, BUFFER_SIZE - 1, 0);
	if (recbytes <= 0) {
		close(clientsock);
		return;
	}

	buffer[recbytes] = '\0';

	/* Parse HTTP request */
	/* method is unused, for now */
	terminatepath(path, 256);

	sscanf(buffer, "%15s %255s", method, path);

	if (strncmp(path, PATH, sizeof(PATH) - 1) == 0) {
		int imgid = (int)randrange(0.0, NUM_IMAGES);
		size_t imgsz = 0;
		double hue, saturation, brightness;
		char hip[INET_ADDRSTRLEN];
		char *hhip;
		unsigned char *imgdata;

		hue = randrange(10.0, 150.0);
		saturation = randrange(10.0, 200.0);
		brightness = randrange(30.0, 150.0);

		inet_ntop(AF_INET, &(args->addr.sin_addr), hip, sizeof(hip));
		hhip = getheader(buffer, "X-Forwarded-For:");
		if (!hhip) hhip = hip;

		imgdata = imggen(imgbuffs[imgid].data,
		                 imgbuffs[imgid].size,
				 path + sizeof(PATH) - 1, &imgsz,
		                 hue, saturation, brightness);

		printf("{ \"forwarded-for\": \"%s\", \"host\": \"%s\", \"image\": \"%d\", "
		       "\"path\": \"%s\", \"size\": \"%zu\", \"hue\": \"%.2f\", "
		       "\"saturation\": \"%.2f\", \"brightness\": \"%.2f\" }\n",
		       hhip, hip, imgid, path + sizeof(PATH) - 1, imgsz, hue, saturation, brightness);

		if (imgdata) {
			snprintf(header, sizeof(header),
				 "HTTP/1.1 200 OK\r\n"
				 "Content-Type: image/jpeg\r\n"
				 "Content-Length: %zu\r\n"
				 "Connection: close\r\n\r\n",
				 imgsz);

			send(clientsock, header, strlen(header), 0);
			send(clientsock, imgdata, imgsz, 0);
			MagickRelinquishMemory(imgdata);
		} else {
			fprintf(stderr, "failed to process image %d\n", imgid);
			send(clientsock, response_404, sizeof(response_404), 0);
		}
	} else {
		send(clientsock, response_404, sizeof(response_404), 0);
	}

	close(clientsock);
	return;
}

void *
worker(void *unused)
{
	(void)unused;
	ThreadArgs args;

	for (;;) {
		pthread_mutex_lock(&queue.lock);

		while (queue.count == 0) {
			pthread_cond_wait(&queue.note, &queue.lock);
		}

		args.sock = queue.clients[queue.head].sock;
		args.addr = queue.clients[queue.head].addr;
		queue.head = (queue.head + 1) % MAX_QUEUE;
		queue.count--;

		pthread_mutex_unlock(&queue.lock);

		handleclient(&args);
	}

	return NULL;
}

int
main(int argc, char *argv[])
{
	int c, efd, opt, serversock;
	const char *progname = argv[0];
	pthread_t threads[NUM_THREADS];
	struct sockaddr_in server_addr = { 0 };

#ifdef __linux__
	struct epoll_event event, events[1];
#else
	struct kevent kev, kevs[10];
#endif

	while ((c = getopt(argc, argv, "d")) >= 0) {
		switch (c) {
		case 'd':
			if (daemon(0, 0) < 0) err(1, "daemon");
			break;
		default:
			fprintf(stderr, "usage: %s [-d]\n", progname);
			exit(2);
		}
	}

	MagickWandGenesis();
	seedinit();

	signal(SIGPIPE, SIG_IGN);

	serversock = socket(AF_INET, SOCK_STREAM, 0);
	if (serversock < 0)
		err(1, "socket");

	if (imgpreload() < 0)
		errx(1, "failed to preload images");

	fonturi = fontpreload();
	if (!fonturi)
		errx(1, "failed to preload the font");

	opt = 1;
	setsockopt(serversock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = INADDR_ANY;
	server_addr.sin_port = htons(PORT);

	if (bind(serversock, (struct sockaddr *) &server_addr,
	         sizeof(server_addr)) < 0)
		err(1, "bind");

	if (listen(serversock, 1024) < 0)
		err(1, "listen");

	printf("Server listening on port %d\n", PORT);
	printf("Access images at: http://localhost:%d" PATH "\n", PORT);

	pthread_mutex_init(&queue.lock, NULL);
	pthread_cond_init(&queue.note, NULL);

	for (int i = 0; i < NUM_THREADS; i++) {
		pthread_create(&threads[i], NULL, worker, NULL);
		pthread_detach(threads[i]);
	}

#ifdef __linux__
	if ((efd = epoll_create1(0)) < 0)
		err(1, "epoll_create1");

	event.events = EPOLLIN;
	event.data.fd = serversock;
	epoll_ctl(efd, EPOLL_CTL_ADD, serversock, &event);
#else
	if ((efd = kqueue()) < 0)
		err(1, "kqueue");

	EV_SET(&kev, serversock, EVFILT_READ, EV_ADD, 0, 0, NULL);
	if (kevent(efd, &kev, 1, NULL, 0, NULL) < 0)
		err(1, "kevent");
#endif


	for (;;) {
		int nfds;

#ifdef __linux__
		if ((nfds = epoll_wait(efd, events, 1 , -1)) < 0) {
			perror("epoll_wait");
			continue;
		}


		if (events[0].events & EPOLLIN) {
#else
		if ((nfds = kevent(efd, NULL, 0, kevs, 10, NULL)) < 0) {
			perror("kevent");
			continue;
		}

		for (int i = 0; i < nfds; i++) {
			if(kevs[i].ident == (uintptr_t)serversock) {
#endif
		/*
		 * This goes inside the for loop above in case of kqueue/kevent
		 */
			ThreadArgs args;
			socklen_t clientaddrlen = sizeof(args.addr);

			args.sock = accept(serversock, (struct sockaddr *)&args.addr,
			                   &clientaddrlen);

			if (args.sock < 0) {
				perror("accept");
				continue;
			}

			pthread_mutex_lock(&queue.lock);

			if (queue.count >= MAX_QUEUE) {
				close(args.sock);
				fprintf(stderr, "Client queue full, rejecting\n");
			} else {
				queue.clients[queue.tail].sock = args.sock;
				queue.clients[queue.tail].addr = args.addr;
				queue.tail = (queue.tail + 1) % MAX_QUEUE;
				queue.count++;
				pthread_cond_signal(&queue.note);
			}

			pthread_mutex_unlock(&queue.lock);
		}
#ifndef __linux__
		} /* closing the kevent for loop */
#endif
	}
	close(efd);
	close(serversock);
	MagickWandTerminus();

	return 0;
}
